@xleddyl/nuxt-cms 0.1.22 → 0.1.24

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 (76) hide show
  1. package/README.md +2 -2
  2. package/dist/module.d.mts +2 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +20 -3
  5. package/dist/runtime/app/components/cms/BlocksField.vue +2 -9
  6. package/dist/runtime/app/components/cms/Button.vue +4 -2
  7. package/dist/runtime/app/components/cms/ConfirmModal.vue +5 -14
  8. package/dist/runtime/app/components/cms/Drawer.d.vue.ts +30 -0
  9. package/dist/runtime/app/components/cms/Drawer.vue +65 -0
  10. package/dist/runtime/app/components/cms/Drawer.vue.d.ts +30 -0
  11. package/dist/runtime/app/components/cms/DropdownMenu.d.vue.ts +0 -3
  12. package/dist/runtime/app/components/cms/DropdownMenu.vue +2 -7
  13. package/dist/runtime/app/components/cms/DropdownMenu.vue.d.ts +0 -3
  14. package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +1 -0
  15. package/dist/runtime/app/components/cms/EmptyState.vue +6 -7
  16. package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +1 -0
  17. package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +21 -0
  18. package/dist/runtime/app/components/cms/EntryDrawer.vue +193 -0
  19. package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +21 -0
  20. package/dist/runtime/app/components/cms/EntryForm.vue +19 -7
  21. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +1 -0
  22. package/dist/runtime/app/components/cms/FieldInput.vue +13 -27
  23. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +1 -0
  24. package/dist/runtime/app/components/cms/Form.vue +1 -1
  25. package/dist/runtime/app/components/cms/FormField.d.vue.ts +4 -5
  26. package/dist/runtime/app/components/cms/FormField.vue +9 -7
  27. package/dist/runtime/app/components/cms/FormField.vue.d.ts +4 -5
  28. package/dist/runtime/app/components/cms/JsonField.vue +3 -4
  29. package/dist/runtime/app/components/cms/LocaleSwitch.d.vue.ts +14 -0
  30. package/dist/runtime/app/components/cms/LocaleSwitch.vue +29 -0
  31. package/dist/runtime/app/components/cms/LocaleSwitch.vue.d.ts +14 -0
  32. package/dist/runtime/app/components/cms/MediaField.vue +7 -17
  33. package/dist/runtime/app/components/cms/MediaGallery.vue +57 -80
  34. package/dist/runtime/app/components/cms/MediaThumb.d.vue.ts +6 -0
  35. package/dist/runtime/app/components/cms/MediaThumb.vue +40 -0
  36. package/dist/runtime/app/components/cms/MediaThumb.vue.d.ts +6 -0
  37. package/dist/runtime/app/components/cms/Modal.d.vue.ts +1 -4
  38. package/dist/runtime/app/components/cms/Modal.vue +8 -7
  39. package/dist/runtime/app/components/cms/Modal.vue.d.ts +1 -4
  40. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +1 -1
  41. package/dist/runtime/app/components/cms/PageHeader.vue +6 -6
  42. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +1 -1
  43. package/dist/runtime/app/components/cms/RelationField.vue +0 -4
  44. package/dist/runtime/app/components/cms/SelectMenu.vue +23 -3
  45. package/dist/runtime/app/components/cms/SlugField.vue +1 -2
  46. package/dist/runtime/app/components/cms/Spinner.d.vue.ts +3 -0
  47. package/dist/runtime/app/components/cms/Spinner.vue +5 -0
  48. package/dist/runtime/app/components/cms/Spinner.vue.d.ts +3 -0
  49. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +6 -0
  50. package/dist/runtime/app/components/cms/StatusBadge.vue +11 -0
  51. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +6 -0
  52. package/dist/runtime/app/components/cms/Table.d.vue.ts +3 -6
  53. package/dist/runtime/app/components/cms/Table.vue +72 -27
  54. package/dist/runtime/app/components/cms/Table.vue.d.ts +3 -6
  55. package/dist/runtime/app/components/cms/Toaster.vue +4 -4
  56. package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +1 -0
  57. package/dist/runtime/app/components/cms/TranslatableField.vue +7 -44
  58. package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +1 -0
  59. package/dist/runtime/app/composables/cms-runtime.d.ts +2 -1
  60. package/dist/runtime/app/layouts/cms-admin.vue +10 -20
  61. package/dist/runtime/app/pages/admin-collection.vue +130 -103
  62. package/dist/runtime/app/pages/admin-entry.vue +6 -24
  63. package/dist/runtime/app/pages/admin-login.vue +12 -31
  64. package/dist/runtime/app/pages/admin-media.vue +8 -2
  65. package/dist/runtime/app/utils/ui.d.ts +0 -7
  66. package/dist/runtime/app/utils/ui.js +0 -5
  67. package/dist/runtime/assets/main.css +1 -1
  68. package/dist/runtime/server/api/collection.get.js +8 -3
  69. package/dist/runtime/server/api/media.post.js +8 -2
  70. package/dist/runtime/server/api/media.put.js +3 -2
  71. package/dist/runtime/server/utils/media.d.ts +4 -1
  72. package/dist/runtime/server/utils/media.js +16 -3
  73. package/dist/runtime/server/utils/relations.d.ts +1 -0
  74. package/dist/runtime/server/utils/relations.js +25 -0
  75. package/dist/runtime/shared/index.d.ts +11 -3
  76. package/package.json +1 -1
@@ -3,7 +3,6 @@
3
3
  :id="formId"
4
4
  :state="state"
5
5
  :schema="schema"
6
- class="flex flex-col gap-5"
7
6
  @submit="emit('submit')"
8
7
  @error="emit('error')"
9
8
  >
@@ -13,22 +12,28 @@
13
12
  :label="field.label"
14
13
  :name="key"
15
14
  :required="field.required"
16
- :ui="CMS_FIELD_UI"
17
15
  >
16
+ <template v-if="isTranslatableField(field) && i18n.locales.length > 1" #label-actions>
17
+ <CmsLocaleSwitch
18
+ :model-value="localeFor(key)"
19
+ :value="state[key]"
20
+ @update:model-value="(locale) => setLocale(key, locale)"
21
+ />
22
+ </template>
18
23
  <CmsFieldInput
19
24
  v-model="state[key]"
20
25
  :field="field"
26
+ :locale="localeFor(key)"
21
27
  :slug-source="field.from ? state[field.from] : void 0"
22
28
  />
23
29
  </CmsFormField>
24
- <div v-if="footer" class="flex items-center justify-end gap-3 pt-1">
25
- <CmsButton type="submit" label="Save" :loading="loading" class="rounded-full px-6" />
30
+ <div v-if="footer" class="cms-form-actions">
31
+ <CmsButton type="submit" label="Save" :loading="loading" />
26
32
  <CmsButton
27
33
  v-if="drafts"
28
34
  type="submit"
29
35
  :label="published ? 'Make draft' : 'Publish'"
30
36
  :loading="loading"
31
- class="rounded-full px-6"
32
37
  @click="togglePublished"
33
38
  />
34
39
  </div>
@@ -36,10 +41,10 @@
36
41
  </template>
37
42
 
38
43
  <script setup>
39
- import { computed } from "#imports";
44
+ import { isTranslatableField } from "#nuxt-cms";
45
+ import { computed, ref } from "#imports";
40
46
  import { buildEntrySchema } from "../../../shared/validation";
41
47
  import { useCmsRuntime } from "../../composables/cms-runtime";
42
- import { CMS_FIELD_UI } from "../../utils/ui";
43
48
  const props = defineProps({
44
49
  fields: { type: Object, required: true },
45
50
  drafts: { type: Boolean, required: false },
@@ -50,6 +55,13 @@ const props = defineProps({
50
55
  const state = defineModel({ type: Object, ...{ required: true } });
51
56
  const emit = defineEmits(["submit", "error"]);
52
57
  const { i18n } = useCmsRuntime();
58
+ const activeLocale = ref({});
59
+ function localeFor(key) {
60
+ return activeLocale.value[key] ?? i18n.defaultLocale;
61
+ }
62
+ function setLocale(key, locale) {
63
+ activeLocale.value = { ...activeLocale.value, [key]: locale };
64
+ }
53
65
  const schema = computed(
54
66
  () => buildEntrySchema({ fields: props.fields, drafts: props.drafts }, i18n)
55
67
  );
@@ -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,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,8 +6,9 @@
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
+ v-if="!readOnly"
11
12
  :multiple="!selectable"
12
13
  :dense="selectable"
13
14
  :media-type="mediaType"
@@ -15,7 +16,7 @@
15
16
  @uploaded="onUploaded"
16
17
  />
17
18
 
18
- <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">
19
20
  <button
20
21
  v-for="f in filters"
21
22
  :key="f"
@@ -27,7 +28,7 @@
27
28
  {{ filterLabels[f] }}
28
29
  </button>
29
30
  <template v-if="folders.length">
30
- <span class="mx-1 h-4 w-px bg-(--cms-line)" />
31
+ <span class="cms-media-filter-separator" />
31
32
  <button
32
33
  v-for="f in folders"
33
34
  :key="f"
@@ -36,40 +37,29 @@
36
37
  :class="{ 'is-active': folder === f }"
37
38
  @click="folder = folder === f ? null : f"
38
39
  >
39
- <CmsIcon name="folder" class="mr-1 size-3" />{{ f }}
40
+ <CmsIcon name="folder" class="size-3" />{{ f }}
40
41
  </button>
41
42
  </template>
42
43
  </div>
43
44
 
44
- <div v-if="loading" class="flex justify-center py-10">
45
- <CmsIcon name="arrow-path" class="size-6 animate-spin text-(--ui-text-dimmed)" />
46
- </div>
45
+ <CmsSpinner v-if="loading" />
47
46
 
48
- <div
49
- v-else-if="visible.length"
50
- class="grid gap-4"
51
- :class="
52
- selectable ? 'grid-cols-2 sm:grid-cols-3' : 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
53
- "
54
- >
47
+ <div v-else-if="visible.length" class="cms-media-grid" :class="{ 'is-compact': selectable }">
55
48
  <component
56
49
  :is="selectable ? 'button' : 'div'"
57
- v-for="item in visible"
50
+ v-for="item in paged"
58
51
  :key="item.key"
59
52
  :type="selectable ? 'button' : void 0"
60
- class="cms-card group overflow-hidden text-left"
61
- :class="{
62
- 'hover:border-(--cms-line-strong)': selectable
63
- }"
53
+ class="cms-card cms-media-tile"
54
+ :class="{ 'is-selectable': selectable }"
64
55
  @click="selectable && emit('select', { key: item.key, url: item.url })"
65
56
  >
66
- <div class="relative flex aspect-square items-center justify-center bg-(--cms-field)">
57
+ <div class="cms-media-preview">
67
58
  <img
68
59
  v-if="item.type === 'image' && item.url"
69
60
  :src="item.url"
70
61
  alt=""
71
62
  loading="lazy"
72
- class="absolute inset-0 size-full object-cover"
73
63
  />
74
64
  <video
75
65
  v-else-if="item.type === 'video' && item.url"
@@ -77,81 +67,66 @@
77
67
  preload="metadata"
78
68
  muted
79
69
  playsinline
80
- class="absolute inset-0 size-full object-cover"
81
- />
82
- <CmsIcon
83
- v-else
84
- :name="mediaIconFor(item.type)"
85
- class="size-8 text-(--ui-text-dimmed)"
86
70
  />
87
- <div
88
- v-if="!selectable"
89
- class="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
90
- >
71
+ <CmsIcon v-else :name="mediaIconFor(item.type)" class="size-8" />
72
+ <div v-if="!selectable" class="cms-media-actions">
91
73
  <CmsButton
74
+ v-if="!readOnly"
92
75
  icon="pencil-square"
93
76
  size="xs"
94
77
  color="neutral"
95
78
  variant="solid"
96
- class="rounded-full"
97
79
  aria-label="Edit details"
98
80
  @click="openEdit(item)"
99
81
  />
100
82
  <CmsButton
101
- icon="document-duplicate"
102
- size="xs"
103
- color="neutral"
104
- variant="solid"
105
- class="rounded-full"
106
- aria-label="Copy URL"
107
- @click="copy(item)"
108
- />
109
- <CmsButton
83
+ v-if="!readOnly"
110
84
  icon="trash"
111
85
  size="xs"
112
86
  color="error"
113
87
  variant="solid"
114
- class="rounded-full"
115
88
  aria-label="Delete"
116
89
  @click="remove(item)"
117
90
  />
118
91
  </div>
119
- </div>
120
- <div class="flex flex-col gap-1 px-3.5 py-2.5">
121
- <div class="truncate text-sm font-medium" :title="item.key">
122
- {{ mediaFilename(item.key) }}
123
- </div>
124
- <div class="cms-kicker">
92
+ <div class="cms-media-info">
125
93
  {{ extension(item.key) }} · {{ formatSize(item.size)
126
94
  }}<template v-if="item.width && item.height">
127
95
  · {{ item.width }}×{{ item.height }}
128
96
  </template>
129
97
  </div>
130
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>
131
104
  </component>
132
105
  </div>
133
106
 
134
107
  <CmsEmptyState v-else-if="loadError" icon="exclamation-triangle" title="Could not load media">
135
- <CmsButton
136
- label="Retry"
137
- icon="arrow-path"
138
- variant="subtle"
139
- class="mt-3 rounded-full px-4"
140
- @click="reload"
141
- />
108
+ <CmsButton label="Retry" icon="arrow-path" variant="subtle" @click="reload" />
142
109
  </CmsEmptyState>
143
110
 
144
111
  <CmsEmptyState
145
112
  v-else
146
113
  icon="photo"
147
- title="No media yet"
148
- body="Files you upload will show up here."
114
+ :title="readOnly ? 'No media' : 'No media yet'"
115
+ :body="readOnly ? 'No media registered.' : 'Files you upload will show up here.'"
116
+ />
117
+
118
+ <CmsPagination
119
+ v-if="!loading && visible.length"
120
+ v-model:page="page"
121
+ :total="visible.length"
122
+ :items-per-page="PAGE_SIZE"
149
123
  />
150
124
 
151
125
  <CmsModal
126
+ v-if="!readOnly"
152
127
  :open="!!editing"
153
128
  :title="editing ? mediaFilename(editing.key) : ''"
154
- :ui="CMS_MODAL_UI"
129
+ size="lg"
155
130
  @update:open="
156
131
  (open) => {
157
132
  if (!open) editing = null;
@@ -159,20 +134,15 @@
159
134
  "
160
135
  >
161
136
  <template #body>
162
- <div class="flex flex-col gap-4">
137
+ <div class="cms-form">
163
138
  <CmsFormField label="Alt text">
164
- <CmsInput v-model="editAlt" size="lg" class="w-full" />
139
+ <CmsInput v-model="editAlt" />
165
140
  </CmsFormField>
166
141
  <CmsFormField label="Folder">
167
- <CmsInput v-model="editFolder" size="lg" class="w-full" />
142
+ <CmsInput v-model="editFolder" />
168
143
  </CmsFormField>
169
- <div class="flex justify-end">
170
- <CmsButton
171
- label="Save"
172
- :loading="editSaving"
173
- class="rounded-full px-6"
174
- @click="saveEdit"
175
- />
144
+ <div class="cms-actions is-end">
145
+ <CmsButton label="Save" :loading="editSaving" @click="saveEdit" />
176
146
  </div>
177
147
  </div>
178
148
  </template>
@@ -181,11 +151,12 @@
181
151
  </template>
182
152
 
183
153
  <script setup>
184
- import { computed, onMounted, ref } from "#imports";
154
+ import { computed, onMounted, ref, watch } from "#imports";
185
155
  import { MEDIA_TYPES, mediaFilename, mediaIconFor } from "#nuxt-cms";
186
156
  import { useCmsConfirm } from "../../composables/cms-confirm";
157
+ import { useCmsRuntime } from "../../composables/cms-runtime";
187
158
  import { useCmsToast } from "../../composables/cms-toast";
188
- import { CMS_MODAL_UI, errorMessage } from "../../utils/ui";
159
+ import { errorMessage } from "../../utils/ui";
189
160
  const filterLabels = {
190
161
  all: "All",
191
162
  image: "Images",
@@ -199,6 +170,8 @@ const props = defineProps({
199
170
  });
200
171
  const emit = defineEmits(["select"]);
201
172
  const toast = useCmsToast();
173
+ const runtime = useCmsRuntime();
174
+ const readOnly = computed(() => runtime.mediaStorage === "local");
202
175
  const endpoint = "/api/cms/admin/media";
203
176
  const items = ref([]);
204
177
  const loading = ref(true);
@@ -207,8 +180,8 @@ async function reload() {
207
180
  loading.value = true;
208
181
  errorCode.value = null;
209
182
  try {
210
- const page = await $fetch(endpoint);
211
- items.value = page.items;
183
+ const result = await $fetch(endpoint);
184
+ items.value = result.items;
212
185
  } catch (err) {
213
186
  errorCode.value = err.statusCode ?? 500;
214
187
  } finally {
@@ -234,6 +207,18 @@ const visible = computed(() => {
234
207
  if (folder.value) list = list.filter((item) => item.folder === folder.value);
235
208
  return list;
236
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
+ );
237
222
  function onUploaded(uploaded) {
238
223
  if (props.selectable && uploaded[0]) {
239
224
  emit("select", uploaded[0]);
@@ -288,14 +273,6 @@ function formatSize(bytes) {
288
273
  }
289
274
  return `${Math.round(value / 1024)} TB`;
290
275
  }
291
- async function copy(item) {
292
- try {
293
- await navigator.clipboard.writeText(item.url ?? item.key);
294
- toast.add({ title: "Copied", color: "success" });
295
- } catch {
296
- toast.add({ title: "Copy failed", color: "error" });
297
- }
298
- }
299
276
  const confirmAction = useCmsConfirm();
300
277
  const removing = ref(false);
301
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;