@xleddyl/nuxt-cms 0.1.60 → 0.2.0

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 (142) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +137 -9
  3. package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
  4. package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
  5. package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
  6. package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
  7. package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
  8. package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
  9. package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
  10. package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
  11. package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -0
  12. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
  13. package/dist/runtime/app/components/cms/EntryForm.vue +105 -30
  14. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
  15. package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
  16. package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
  17. package/dist/runtime/app/components/cms/FormField.vue +8 -2
  18. package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
  19. package/dist/runtime/app/components/cms/Icon.vue +1 -1
  20. package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +16 -0
  21. package/dist/runtime/app/components/cms/MediaDetail.vue +169 -0
  22. package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +16 -0
  23. package/dist/runtime/app/components/cms/MediaField.vue +9 -2
  24. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +2 -2
  25. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +55 -54
  26. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +2 -2
  27. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +8 -2
  28. package/dist/runtime/app/components/cms/MediaGallery.vue +927 -336
  29. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +8 -2
  30. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
  31. package/dist/runtime/app/components/cms/MediaUpload.vue +76 -139
  32. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
  33. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
  34. package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
  35. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
  36. package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
  37. package/dist/runtime/app/components/cms/PasswordInput.vue +77 -0
  38. package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
  39. package/dist/runtime/app/components/cms/SelectMenu.vue +3 -5
  40. package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
  41. package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
  42. package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
  43. package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
  44. package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
  45. package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
  46. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
  47. package/dist/runtime/app/components/cms/SettingsEntry.vue +133 -0
  48. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
  49. package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
  50. package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
  51. package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
  52. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
  53. package/dist/runtime/app/components/cms/SettingsModal.vue +179 -0
  54. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
  55. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
  56. package/dist/runtime/app/components/cms/SettingsUsers.vue +270 -0
  57. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
  58. package/dist/runtime/app/components/cms/Spinner.vue +2 -2
  59. package/dist/runtime/app/components/cms/Table.d.vue.ts +7 -2
  60. package/dist/runtime/app/components/cms/Table.vue +38 -1
  61. package/dist/runtime/app/components/cms/Table.vue.d.ts +7 -2
  62. package/dist/runtime/app/components/cms/Toaster.vue +1 -1
  63. package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
  64. package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
  65. package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
  66. package/dist/runtime/app/composables/cms-account.d.ts +11 -0
  67. package/dist/runtime/app/composables/cms-account.js +23 -0
  68. package/dist/runtime/app/composables/cms-media-library.d.ts +75 -0
  69. package/dist/runtime/app/composables/cms-media-library.js +151 -0
  70. package/dist/runtime/app/composables/cms-media-uploader.d.ts +22 -0
  71. package/dist/runtime/app/composables/cms-media-uploader.js +244 -0
  72. package/dist/runtime/app/composables/cms-settings.d.ts +13 -0
  73. package/dist/runtime/app/composables/cms-settings.js +59 -0
  74. package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
  75. package/dist/runtime/app/composables/cms-theme.js +27 -0
  76. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  77. package/dist/runtime/app/layouts/cms-admin.vue +71 -17
  78. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  79. package/dist/runtime/app/pages/admin-collection.vue +127 -101
  80. package/dist/runtime/app/pages/admin-entry.vue +1 -0
  81. package/dist/runtime/app/pages/admin-login.vue +18 -3
  82. package/dist/runtime/app/pages/admin-media.vue +19 -3
  83. package/dist/runtime/app/utils/ui.d.ts +2 -0
  84. package/dist/runtime/app/utils/ui.js +18 -0
  85. package/dist/runtime/assets/main.css +1 -1
  86. package/dist/runtime/server/api/account-password.put.d.ts +4 -0
  87. package/dist/runtime/server/api/account-password.put.js +38 -0
  88. package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
  89. package/dist/runtime/server/api/account-welcome.post.js +14 -0
  90. package/dist/runtime/server/api/account.put.d.ts +3 -0
  91. package/dist/runtime/server/api/account.put.js +25 -0
  92. package/dist/runtime/server/api/media-delete.post.d.ts +4 -0
  93. package/dist/runtime/server/api/media-delete.post.js +23 -0
  94. package/dist/runtime/server/api/media-folder.delete.js +6 -16
  95. package/dist/runtime/server/api/media-folder.patch.d.ts +5 -0
  96. package/dist/runtime/server/api/media-folder.patch.js +68 -0
  97. package/dist/runtime/server/api/media-folder.post.js +3 -15
  98. package/dist/runtime/server/api/media-move.post.d.ts +5 -0
  99. package/dist/runtime/server/api/media-move.post.js +28 -0
  100. package/dist/runtime/server/api/media-usage.post.d.ts +5 -0
  101. package/dist/runtime/server/api/media-usage.post.js +15 -0
  102. package/dist/runtime/server/api/settings.delete.d.ts +4 -0
  103. package/dist/runtime/server/api/settings.delete.js +12 -0
  104. package/dist/runtime/server/api/settings.get.d.ts +3 -0
  105. package/dist/runtime/server/api/settings.get.js +15 -0
  106. package/dist/runtime/server/api/settings.put.d.ts +3 -0
  107. package/dist/runtime/server/api/settings.put.js +14 -0
  108. package/dist/runtime/server/api/user.delete.d.ts +4 -0
  109. package/dist/runtime/server/api/user.delete.js +12 -0
  110. package/dist/runtime/server/api/user.put.d.ts +3 -0
  111. package/dist/runtime/server/api/user.put.js +27 -0
  112. package/dist/runtime/server/api/users.get.d.ts +3 -0
  113. package/dist/runtime/server/api/users.get.js +11 -0
  114. package/dist/runtime/server/api/users.post.d.ts +3 -0
  115. package/dist/runtime/server/api/users.post.js +32 -0
  116. package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
  117. package/dist/runtime/server/plugins/session-validate.js +13 -0
  118. package/dist/runtime/server/routes/auth/login.post.js +18 -8
  119. package/dist/runtime/server/utils/media-folders.d.ts +86 -0
  120. package/dist/runtime/server/utils/media-folders.js +37 -0
  121. package/dist/runtime/server/utils/media-references.d.ts +7 -1
  122. package/dist/runtime/server/utils/media-references.js +40 -0
  123. package/dist/runtime/server/utils/media-usage.d.ts +2 -0
  124. package/dist/runtime/server/utils/media-usage.js +18 -0
  125. package/dist/runtime/server/utils/password.d.ts +3 -0
  126. package/dist/runtime/server/utils/password.js +31 -0
  127. package/dist/runtime/server/utils/require-admin.d.ts +6 -2
  128. package/dist/runtime/server/utils/require-admin.js +33 -28
  129. package/dist/runtime/server/utils/same-origin.d.ts +2 -0
  130. package/dist/runtime/server/utils/same-origin.js +24 -0
  131. package/dist/runtime/server/utils/settings.d.ts +16 -0
  132. package/dist/runtime/server/utils/settings.js +31 -0
  133. package/dist/runtime/server/utils/users.d.ts +15 -0
  134. package/dist/runtime/server/utils/users.js +34 -0
  135. package/dist/runtime/shared/index.d.ts +29 -1
  136. package/dist/runtime/shared/index.js +36 -2
  137. package/dist/runtime/shared/layout.d.ts +34 -0
  138. package/dist/runtime/shared/layout.js +130 -0
  139. package/dist/runtime/shared/users.d.ts +18 -0
  140. package/dist/runtime/shared/users.js +1 -0
  141. package/dist/runtime/shared/validation.js +5 -4
  142. package/package.json +3 -3
@@ -4,16 +4,22 @@ type __VLS_Props = {
4
4
  mediaType?: MediaType | MediaType[];
5
5
  accept?: string[];
6
6
  };
7
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ type __VLS_ModelProps = {
8
+ 'folder'?: string | null;
9
+ };
10
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
11
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
12
  select: (item: {
9
13
  key: string;
10
14
  url: string | null;
11
15
  }) => any;
12
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ "update:folder": (value: string | null) => any;
17
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
18
  onSelect?: ((item: {
14
19
  key: string;
15
20
  url: string | null;
16
21
  }) => any) | undefined;
22
+ "onUpdate:folder"?: ((value: string | null) => any) | undefined;
17
23
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
24
  declare const _default: typeof __VLS_export;
19
25
  export default _default;
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  mediaType?: MediaType | MediaType[];
5
5
  accept?: string[];
6
6
  folder?: string | null;
7
+ directories?: boolean;
7
8
  };
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
10
  uploaded: (items: MediaItem[]) => any;
@@ -1,165 +1,102 @@
1
1
  <template>
2
- <button
3
- type="button"
4
- class="cms-dropzone"
5
- :class="{ 'is-over': dragOver, 'is-busy': uploading }"
6
- :disabled="uploading"
7
- @click="input?.click()"
8
- @dragover.prevent="dragOver = true"
9
- @dragleave.prevent="dragOver = false"
10
- @drop.prevent="onDrop"
11
- >
12
- <input
13
- ref="input"
14
- type="file"
15
- class="hidden"
16
- :accept="acceptAttr"
17
- :multiple="multiple"
18
- @change="onChange"
19
- @click.stop
20
- />
21
- <CmsIcon
22
- :name="uploading ? 'arrow-path' : 'cloud-arrow-up'"
23
- class="size-5 shrink-0"
24
- :class="{ 'animate-spin': uploading }"
25
- />
26
- <span class="text-sm font-medium">
27
- {{
28
- uploading ? `Uploading ${Math.min(done + 1, total)} of ${total}\u2026` : multiple ? "Drop files here or click to browse" : "Drop a file here or click to browse"
29
- }}
30
- </span>
31
- <span v-if="!uploading" class="cms-dropzone-hint">
32
- <CmsIcon :name="folder ? 'folder' : 'circle-stack'" class="size-3.5" />
33
- {{ folder ?? "Library root" }}
34
- </span>
35
- </button>
2
+ <div class="flex flex-col items-center gap-2">
3
+ <button
4
+ type="button"
5
+ class="cms-dropzone"
6
+ :class="{ 'is-over': dragOver, 'is-busy': uploading }"
7
+ :disabled="uploading"
8
+ @click="input?.click()"
9
+ @dragover.prevent="dragOver = true"
10
+ @dragleave.prevent="dragOver = false"
11
+ @drop.prevent.stop="onDrop"
12
+ >
13
+ <input
14
+ ref="input"
15
+ type="file"
16
+ class="hidden"
17
+ :accept="acceptAttr"
18
+ :multiple="multiple"
19
+ @change="onChange"
20
+ @click.stop
21
+ />
22
+ <CmsIcon
23
+ :name="uploading ? 'arrow-path' : 'cloud-arrow-up'"
24
+ class="size-5 shrink-0"
25
+ :class="{ 'animate-spin': uploading }"
26
+ />
27
+ <span class="text-sm font-medium">
28
+ {{
29
+ uploading ? `Uploading ${Math.min(done + 1, total)} of ${total}\u2026` : multiple ? "Drop files here or click to browse" : "Drop a file here or click to browse"
30
+ }}
31
+ </span>
32
+ <span v-if="!uploading" class="cms-dropzone-hint">
33
+ <CmsIcon :name="folder ? 'folder' : 'circle-stack'" class="size-3.5" />
34
+ {{ folder ?? "Library root" }}
35
+ </span>
36
+ </button>
37
+ <template v-if="directories && multiple && !uploading">
38
+ <input
39
+ ref="directoryInput"
40
+ type="file"
41
+ class="hidden"
42
+ webkitdirectory
43
+ multiple
44
+ @change="onChange"
45
+ />
46
+ <CmsButton
47
+ label="Upload a folder"
48
+ icon="folder-arrow-down"
49
+ variant="ghost"
50
+ color="neutral"
51
+ size="sm"
52
+ @click="directoryInput?.click()"
53
+ />
54
+ </template>
55
+ </div>
36
56
  </template>
37
57
 
38
58
  <script setup>
39
- import { formatFileSize, mediaTypeAccept } from "#nuxt-cms";
59
+ import { mediaTypeAccept } from "#nuxt-cms";
40
60
  import { computed, ref } from "#imports";
41
- import { useCmsRuntime } from "../../composables/cms-runtime";
42
- import { useCmsToast } from "../../composables/cms-toast";
61
+ import {
62
+ uploadTreeFromDrop,
63
+ uploadTreeFromFiles,
64
+ useCmsMediaUploader
65
+ } from "../../composables/cms-media-uploader";
43
66
  const props = defineProps({
44
67
  multiple: { type: Boolean, required: false },
45
68
  mediaType: { type: null, required: false, default: "file" },
46
69
  accept: { type: Array, required: false },
47
- folder: { type: [String, null], required: false, default: null }
70
+ folder: { type: [String, null], required: false, default: null },
71
+ directories: { type: Boolean, required: false }
48
72
  });
49
73
  const emit = defineEmits(["uploaded"]);
50
- const toast = useCmsToast();
51
- const { mediaMaxFileSize } = useCmsRuntime();
52
- async function imageDimensions(file) {
53
- try {
54
- const bitmap = await createImageBitmap(file);
55
- const dims = { width: bitmap.width, height: bitmap.height };
56
- bitmap.close();
57
- return dims;
58
- } catch {
59
- return {};
60
- }
61
- }
62
- function videoDimensions(file) {
63
- return new Promise((resolve) => {
64
- const url = URL.createObjectURL(file);
65
- const video = document.createElement("video");
66
- const settle = (dims) => {
67
- URL.revokeObjectURL(url);
68
- resolve(dims);
69
- };
70
- video.preload = "metadata";
71
- video.muted = true;
72
- video.onloadedmetadata = () => settle(
73
- video.videoWidth && video.videoHeight ? { width: video.videoWidth, height: video.videoHeight } : {}
74
- );
75
- video.onerror = () => settle({});
76
- video.src = url;
77
- });
78
- }
79
- async function mediaDimensions(file) {
80
- if (file.type.startsWith("image/")) return imageDimensions(file);
81
- if (file.type.startsWith("video/")) return videoDimensions(file);
82
- return {};
83
- }
74
+ const { upload, uploading, done, total } = useCmsMediaUploader();
84
75
  const input = ref(null);
76
+ const directoryInput = ref(null);
85
77
  const dragOver = ref(false);
86
- const uploading = ref(false);
87
- const done = ref(0);
88
- const total = ref(0);
89
78
  const acceptAttr = computed(
90
79
  () => (props.accept?.length ? props.accept : mediaTypeAccept(props.mediaType))?.join(",")
91
80
  );
92
- function matchesAccept(file) {
93
- if (!acceptAttr.value) return true;
94
- return acceptAttr.value.split(",").some((raw) => {
95
- const pattern = raw.trim().toLowerCase();
96
- if (pattern.endsWith("/*")) return file.type.toLowerCase().startsWith(pattern.slice(0, -1));
97
- if (pattern.startsWith(".")) return file.name.toLowerCase().endsWith(pattern);
98
- return file.type.toLowerCase() === pattern;
81
+ async function handleTree(pending) {
82
+ const tree = await pending;
83
+ const items = await upload(tree, {
84
+ folder: props.folder,
85
+ accept: acceptAttr.value,
86
+ multiple: props.multiple
99
87
  });
100
- }
101
- async function uploadOne(file) {
102
- const presign = await $fetch("/api/cms/admin/media/presign", {
103
- method: "POST",
104
- body: {
105
- filename: file.name,
106
- contentType: file.type || "application/octet-stream",
107
- size: file.size,
108
- folder: props.folder
109
- }
110
- });
111
- const res = await fetch(presign.uploadUrl, {
112
- method: "PUT",
113
- headers: presign.headers,
114
- body: file
115
- });
116
- if (!res.ok) throw new Error(`Upload failed (${res.status})`);
117
- const item = await $fetch("/api/cms/admin/media", {
118
- method: "POST",
119
- body: {
120
- key: presign.key,
121
- folder: presign.folder,
122
- mime: file.type || null,
123
- size: file.size,
124
- ...await mediaDimensions(file)
125
- }
126
- });
127
- done.value++;
128
- return item;
129
- }
130
- async function handleFiles(list) {
131
- let files = Array.from(list).filter(matchesAccept);
132
- const tooLarge = files.filter((file) => file.size > mediaMaxFileSize);
133
- for (const file of tooLarge) {
134
- toast.add({
135
- title: `File too large (max ${formatFileSize(mediaMaxFileSize)}): ${file.name}`,
136
- color: "error"
137
- });
138
- }
139
- files = files.filter((file) => file.size <= mediaMaxFileSize);
140
- if (!props.multiple) files = files.slice(0, 1);
141
- if (!files.length || uploading.value) return;
142
- uploading.value = true;
143
- done.value = 0;
144
- total.value = files.length;
145
- try {
146
- const results = await Promise.allSettled(files.map(uploadOne));
147
- const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
148
- const failed = results.length - ok.length;
149
- if (failed)
150
- toast.add({ title: `${failed} upload${failed > 1 ? "s" : ""} failed`, color: "error" });
151
- if (ok.length) emit("uploaded", ok);
152
- } finally {
153
- uploading.value = false;
154
- }
88
+ if (items.length || props.directories && tree.directories.length) emit("uploaded", items);
155
89
  }
156
90
  function onChange(event) {
157
91
  const el = event.target;
158
- if (el.files?.length) void handleFiles(el.files);
92
+ if (el.files?.length) void handleTree(uploadTreeFromFiles(el.files));
159
93
  el.value = "";
160
94
  }
161
95
  function onDrop(event) {
162
96
  dragOver.value = false;
163
- if (event.dataTransfer?.files.length) void handleFiles(event.dataTransfer.files);
97
+ if (!event.dataTransfer?.files.length) return;
98
+ void handleTree(
99
+ props.directories ? uploadTreeFromDrop(event.dataTransfer) : uploadTreeFromFiles(event.dataTransfer.files)
100
+ );
164
101
  }
165
102
  </script>
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  mediaType?: MediaType | MediaType[];
5
5
  accept?: string[];
6
6
  folder?: string | null;
7
+ directories?: boolean;
7
8
  };
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
10
  uploaded: (items: MediaItem[]) => any;
@@ -4,13 +4,15 @@ export interface CmsBreadcrumbLink {
4
4
  }
5
5
  type __VLS_Props = {
6
6
  title: string;
7
+ icon?: string;
8
+ description?: string;
7
9
  links?: CmsBreadcrumbLink[];
8
10
  };
9
- declare var __VLS_12: {}, __VLS_14: {};
11
+ declare var __VLS_17: {}, __VLS_19: {};
10
12
  type __VLS_Slots = {} & {
11
- badge?: (props: typeof __VLS_12) => any;
13
+ badge?: (props: typeof __VLS_17) => any;
12
14
  } & {
13
- default?: (props: typeof __VLS_14) => any;
15
+ default?: (props: typeof __VLS_19) => any;
14
16
  };
15
17
  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>;
16
18
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -4,14 +4,22 @@
4
4
  <nav v-if="links?.length" class="cms-breadcrumb">
5
5
  <template v-for="link in links" :key="link.to">
6
6
  <NuxtLink :to="link.to" class="cms-breadcrumb-link">{{ link.label }}</NuxtLink>
7
- <CmsIcon name="chevron-right" class="cms-breadcrumb-separator size-2.5" />
7
+ <CmsIcon name="chevron-right" class="cms-breadcrumb-separator size-3" />
8
8
  </template>
9
9
  </nav>
10
- <div class="cms-actions">
11
- <h1 class="cms-title cms-title-md">
12
- {{ title }}
13
- </h1>
14
- <slot name="badge" />
10
+ <div class="cms-page-title-row">
11
+ <span v-if="icon" class="cms-page-icon">
12
+ <CmsIcon :name="icon" class="size-[18px]" />
13
+ </span>
14
+ <div class="cms-page-title-text">
15
+ <div class="cms-actions">
16
+ <h1 class="cms-title cms-title-md">
17
+ {{ title }}
18
+ </h1>
19
+ <slot name="badge" />
20
+ </div>
21
+ <p v-if="description" class="cms-page-description">{{ description }}</p>
22
+ </div>
15
23
  </div>
16
24
  </div>
17
25
  <slot />
@@ -21,6 +29,8 @@
21
29
  <script setup>
22
30
  defineProps({
23
31
  title: { type: String, required: true },
32
+ icon: { type: String, required: false },
33
+ description: { type: String, required: false },
24
34
  links: { type: Array, required: false }
25
35
  });
26
36
  </script>
@@ -4,13 +4,15 @@ export interface CmsBreadcrumbLink {
4
4
  }
5
5
  type __VLS_Props = {
6
6
  title: string;
7
+ icon?: string;
8
+ description?: string;
7
9
  links?: CmsBreadcrumbLink[];
8
10
  };
9
- declare var __VLS_12: {}, __VLS_14: {};
11
+ declare var __VLS_17: {}, __VLS_19: {};
10
12
  type __VLS_Slots = {} & {
11
- badge?: (props: typeof __VLS_12) => any;
13
+ badge?: (props: typeof __VLS_17) => any;
12
14
  } & {
13
- default?: (props: typeof __VLS_14) => any;
15
+ default?: (props: typeof __VLS_19) => any;
14
16
  };
15
17
  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>;
16
18
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -0,0 +1,17 @@
1
+ type __VLS_Props = {
2
+ generate?: boolean;
3
+ copy?: boolean;
4
+ placeholder?: string;
5
+ autocomplete?: string;
6
+ };
7
+ type __VLS_ModelProps = {
8
+ modelValue: string;
9
+ };
10
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
11
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "update:modelValue": (value: string) => any;
13
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
14
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <CmsInput
3
+ v-model="model"
4
+ :type="visible ? 'text' : 'password'"
5
+ icon="key"
6
+ class="cms-password-input"
7
+ :autocomplete="autocomplete ?? 'new-password'"
8
+ :placeholder="placeholder"
9
+ >
10
+ <template #trailing>
11
+ <span class="cms-password-actions">
12
+ <button
13
+ type="button"
14
+ class="cms-password-action"
15
+ :title="visible ? 'Hide password' : 'Show password'"
16
+ :aria-label="visible ? 'Hide password' : 'Show password'"
17
+ @click="visible = !visible"
18
+ >
19
+ <CmsIcon :name="visible ? 'eye-slash' : 'eye'" class="size-4" />
20
+ </button>
21
+ <button
22
+ v-if="generate"
23
+ type="button"
24
+ class="cms-password-action"
25
+ title="Generate a password"
26
+ aria-label="Generate a password"
27
+ @click="regenerate"
28
+ >
29
+ <CmsIcon name="sparkles" class="size-4" />
30
+ </button>
31
+ <button
32
+ v-if="copy"
33
+ type="button"
34
+ class="cms-password-action"
35
+ :class="{ 'is-done': copied }"
36
+ :title="copied ? 'Copied' : 'Copy password'"
37
+ :aria-label="copied ? 'Copied' : 'Copy password'"
38
+ :disabled="!model"
39
+ @click="copyPassword"
40
+ >
41
+ <CmsIcon
42
+ :name="copied ? 'clipboard-document-check' : 'clipboard-document'"
43
+ class="size-4"
44
+ />
45
+ </button>
46
+ </span>
47
+ </template>
48
+ </CmsInput>
49
+ </template>
50
+
51
+ <script setup>
52
+ import { ref } from "#imports";
53
+ import { generatePassword } from "../../composables/cms-account";
54
+ defineProps({
55
+ generate: { type: Boolean, required: false },
56
+ copy: { type: Boolean, required: false },
57
+ placeholder: { type: String, required: false },
58
+ autocomplete: { type: String, required: false }
59
+ });
60
+ const model = defineModel({ type: String, ...{ required: true } });
61
+ const visible = ref(false);
62
+ const copied = ref(false);
63
+ let copiedTimer;
64
+ function regenerate() {
65
+ model.value = generatePassword();
66
+ visible.value = true;
67
+ }
68
+ async function copyPassword() {
69
+ if (!model.value) return;
70
+ await navigator.clipboard.writeText(model.value);
71
+ copied.value = true;
72
+ clearTimeout(copiedTimer);
73
+ copiedTimer = setTimeout(() => {
74
+ copied.value = false;
75
+ }, 1600);
76
+ }
77
+ </script>
@@ -0,0 +1,17 @@
1
+ type __VLS_Props = {
2
+ generate?: boolean;
3
+ copy?: boolean;
4
+ placeholder?: string;
5
+ autocomplete?: string;
6
+ };
7
+ type __VLS_ModelProps = {
8
+ modelValue: string;
9
+ };
10
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
11
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "update:modelValue": (value: string) => any;
13
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
14
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -25,11 +25,9 @@
25
25
  @input="onInput"
26
26
  @focus="onFocus"
27
27
  />
28
- <CmsIcon
29
- v-if="loading"
30
- name="arrow-path"
31
- class="cms-selectmenu-affix ml-auto size-4 animate-spin"
32
- />
28
+ <span v-if="loading" class="cms-selectmenu-affix ml-auto flex">
29
+ <CmsIcon name="arrow-path" class="size-4 animate-spin" />
30
+ </span>
33
31
  <button
34
32
  v-else
35
33
  type="button"
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,138 @@
1
+ <template>
2
+ <div class="cms-settings-entry">
3
+ <div class="cms-settings-entry-header">
4
+ <div class="cms-settings-heading">
5
+ <h2 class="cms-title cms-title-sm">Account</h2>
6
+ <p class="cms-settings-hint">Your profile and your password.</p>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="cms-account-card">
11
+ <span class="cms-avatar is-lg">{{ initial }}</span>
12
+ <div class="cms-account-text">
13
+ <span class="cms-account-name">{{ displayName }}</span>
14
+ <span class="cms-account-email">{{ email }}</span>
15
+ </div>
16
+ <span class="cms-badge is-muted">{{ isSuperAdmin ? "Super admin" : "Admin" }}</span>
17
+ </div>
18
+
19
+ <CmsAlert v-if="isSuperAdmin" color="neutral" title="Environment account">
20
+ This account signs in with the credentials of the environment variables
21
+ (NUXT_CMS_ADMIN_EMAIL and NUXT_CMS_ADMIN_PASSWORD). Change them there. Only this account
22
+ can manage users.
23
+ </CmsAlert>
24
+
25
+ <template v-else>
26
+ <div class="cms-settings-group">
27
+ <div class="cms-settings-heading">
28
+ <p class="cms-settings-label">Profile</p>
29
+ <p class="cms-settings-hint">The name shows in the sidebar.</p>
30
+ </div>
31
+ <CmsForm :state="profile" @submit="saveProfile">
32
+ <div class="cms-form-row" style="--cms-row-cols: 2">
33
+ <CmsFormField label="Name" name="name">
34
+ <CmsInput v-model="profile.name" placeholder="Your name" autocomplete="name" />
35
+ </CmsFormField>
36
+ <CmsFormField label="Email" name="email">
37
+ <CmsInput :model-value="email" disabled />
38
+ </CmsFormField>
39
+ </div>
40
+ <div class="cms-actions is-end">
41
+ <CmsButton
42
+ type="submit"
43
+ label="Save profile"
44
+ size="sm"
45
+ :loading="savingProfile"
46
+ :disabled="profile.name === (user?.name ?? '')"
47
+ />
48
+ </div>
49
+ </CmsForm>
50
+ </div>
51
+
52
+ <div class="cms-settings-group">
53
+ <div class="cms-settings-heading">
54
+ <p class="cms-settings-label">Password</p>
55
+ <p class="cms-settings-hint">
56
+ At least {{ MIN_PASSWORD_LENGTH }} characters. Use the sparkle button to generate
57
+ one.
58
+ </p>
59
+ </div>
60
+ <CmsForm :state="password" @submit="savePassword">
61
+ <CmsAlert v-if="passwordError" color="error" :title="passwordError" />
62
+ <CmsFormField label="Current password" name="current">
63
+ <CmsPasswordInput
64
+ v-model="password.current"
65
+ autocomplete="current-password"
66
+ placeholder="••••••••"
67
+ />
68
+ </CmsFormField>
69
+ <CmsFormField label="New password" name="next">
70
+ <CmsPasswordInput v-model="password.next" generate copy />
71
+ </CmsFormField>
72
+ <div class="cms-actions is-end">
73
+ <CmsButton
74
+ type="submit"
75
+ label="Change password"
76
+ size="sm"
77
+ :loading="savingPassword"
78
+ :disabled="!password.current || password.next.length < MIN_PASSWORD_LENGTH"
79
+ />
80
+ </div>
81
+ </CmsForm>
82
+ </div>
83
+ </template>
84
+ </div>
85
+ </template>
86
+
87
+ <script setup>
88
+ import { MIN_PASSWORD_LENGTH } from "#nuxt-cms";
89
+ import { ref, watch } from "#imports";
90
+ import { useCmsAccount } from "../../composables/cms-account";
91
+ import { useCmsToast } from "../../composables/cms-toast";
92
+ import { cmsApi } from "../../utils/api";
93
+ import { errorMessage } from "../../utils/ui";
94
+ const { user, email, isSuperAdmin, displayName, initial, refresh } = useCmsAccount();
95
+ const toast = useCmsToast();
96
+ const profile = ref({ name: user.value?.name ?? "" });
97
+ watch(
98
+ () => user.value?.name,
99
+ (name) => {
100
+ profile.value.name = name ?? "";
101
+ }
102
+ );
103
+ const savingProfile = ref(false);
104
+ async function saveProfile() {
105
+ savingProfile.value = true;
106
+ try {
107
+ await cmsApi("/api/cms/account", {
108
+ method: "PUT",
109
+ body: { name: profile.value.name || null }
110
+ });
111
+ await refresh();
112
+ toast.add({ title: "Profile saved", color: "success" });
113
+ } catch (error) {
114
+ toast.add({ title: "Profile not saved", description: errorMessage(error), color: "error" });
115
+ } finally {
116
+ savingProfile.value = false;
117
+ }
118
+ }
119
+ const password = ref({ current: "", next: "" });
120
+ const passwordError = ref(null);
121
+ const savingPassword = ref(false);
122
+ async function savePassword() {
123
+ savingPassword.value = true;
124
+ passwordError.value = null;
125
+ try {
126
+ await cmsApi("/api/cms/account/password", {
127
+ method: "PUT",
128
+ body: { currentPassword: password.value.current, newPassword: password.value.next }
129
+ });
130
+ password.value = { current: "", next: "" };
131
+ toast.add({ title: "Password changed", color: "success" });
132
+ } catch (error) {
133
+ passwordError.value = errorMessage(error) ?? "Password not changed.";
134
+ } finally {
135
+ savingPassword.value = false;
136
+ }
137
+ }
138
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import type { FieldConfig } from '#nuxt-cms';
2
+ type __VLS_Props = {
3
+ fields: Record<string, FieldConfig>;
4
+ extra: string[];
5
+ };
6
+ type __VLS_ModelProps = {
7
+ modelValue: string[];
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:modelValue": (value: string[]) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
+ "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;