@xleddyl/nuxt-cms 0.1.27 → 0.1.29

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 (47) hide show
  1. package/README.md +5 -4
  2. package/dist/module.d.mts +37 -30
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +123 -35
  5. package/dist/runtime/app/components/cms/Alert.d.vue.ts +1 -2
  6. package/dist/runtime/app/components/cms/Alert.vue +2 -3
  7. package/dist/runtime/app/components/cms/Alert.vue.d.ts +1 -2
  8. package/dist/runtime/app/components/cms/BlocksField.vue +2 -2
  9. package/dist/runtime/app/components/cms/Button.d.vue.ts +1 -1
  10. package/dist/runtime/app/components/cms/Button.vue.d.ts +1 -1
  11. package/dist/runtime/app/components/cms/ConfirmModal.vue +1 -1
  12. package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -1
  13. package/dist/runtime/app/components/cms/MediaField.vue +2 -2
  14. package/dist/runtime/app/components/cms/MediaGallery.vue +3 -3
  15. package/dist/runtime/app/components/cms/MediaUpload.vue +9 -4
  16. package/dist/runtime/app/components/cms/Modal.d.vue.ts +4 -4
  17. package/dist/runtime/app/components/cms/Modal.vue +4 -4
  18. package/dist/runtime/app/components/cms/Modal.vue.d.ts +4 -4
  19. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +0 -1
  20. package/dist/runtime/app/components/cms/PageHeader.vue +0 -4
  21. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +0 -1
  22. package/dist/runtime/app/components/cms/RichTextField.vue +1 -1
  23. package/dist/runtime/app/components/cms/Toaster.vue +6 -0
  24. package/dist/runtime/app/components/cms/TranslatableField.vue +19 -8
  25. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  26. package/dist/runtime/app/layouts/cms-admin.vue +2 -2
  27. package/dist/runtime/app/pages/admin-collection.vue +12 -7
  28. package/dist/runtime/app/pages/admin-entry.vue +2 -2
  29. package/dist/runtime/app/pages/admin-login.vue +5 -5
  30. package/dist/runtime/assets/main.css +1 -1
  31. package/dist/runtime/server/api/collection.get.d.ts +1 -3
  32. package/dist/runtime/server/api/collection.get.js +3 -2
  33. package/dist/runtime/server/api/collection.post.js +3 -3
  34. package/dist/runtime/server/api/collection.put.js +13 -3
  35. package/dist/runtime/server/api/item.get.js +2 -2
  36. package/dist/runtime/server/api/item.put.js +7 -2
  37. package/dist/runtime/server/api/media-presign.post.js +3 -9
  38. package/dist/runtime/server/utils/graphql.js +13 -1
  39. package/dist/runtime/server/utils/media.d.ts +2 -0
  40. package/dist/runtime/server/utils/media.js +9 -1
  41. package/dist/runtime/server/utils/registry.d.ts +2 -0
  42. package/dist/runtime/server/utils/registry.js +7 -0
  43. package/dist/runtime/shared/index.d.ts +11 -1
  44. package/dist/runtime/shared/index.js +70 -1
  45. package/dist/runtime/shared/validation.js +1 -1
  46. package/dist/types.d.mts +1 -1
  47. package/package.json +4 -2
@@ -14,6 +14,8 @@ export declare function mediaFilename(key: string): string;
14
14
  export declare function mediaTypeFor(mime: string | null | undefined, key: string): MediaType;
15
15
  export declare function mediaIconFor(type: MediaType): string;
16
16
  export declare function mediaPublicUrl(baseUrl: string | null | undefined, key: string): string | null;
17
+ export declare const DEFAULT_MEDIA_MAX_FILE_SIZE: number;
18
+ export declare function formatFileSize(bytes: number): string;
17
19
  export declare function slugify(value: string): string;
18
20
  export declare const MEDIA_FOLDER_MAX_DEPTH = 4;
19
21
  export declare function normalizeMediaFolder(value: string | null | undefined): string | null;
@@ -52,6 +54,13 @@ export interface FieldConfig {
52
54
  onDelete?: 'set null' | 'cascade' | 'restrict';
53
55
  }
54
56
  export declare function isTranslatableField(field: FieldConfig): boolean;
57
+ export declare function isTranslatableMediaField(field: FieldConfig): boolean;
58
+ export declare function decodeTranslatableMedia(value: unknown, defaultLocale: string): Record<string, string> | null;
59
+ export declare function encodeTranslatableMedia(value: unknown): string | null;
60
+ export declare function pickTranslatedMedia(values: Record<string, string> | null | undefined, locale: string, defaultLocale: string): string | null;
61
+ export declare function translatableMediaKeys(entry: Pick<CmsEntry, 'fields'>): string[];
62
+ export declare function encodeEntryTranslatableMedia(entry: Pick<CmsEntry, 'fields'>, values: Record<string, unknown>): Record<string, unknown>;
63
+ export declare function decodeEntryTranslatableMedia<T extends Record<string, unknown>>(entry: Pick<CmsEntry, 'fields'>, rows: T[], defaultLocale: string): T[];
55
64
  export declare function isMultiSelect(field: FieldConfig): boolean;
56
65
  export declare function translatableFieldKeys(entry: CmsEntry): string[];
57
66
  export interface CmsEntry {
@@ -106,6 +115,7 @@ export interface MediaFieldInput extends FieldInputBase {
106
115
  type: 'media';
107
116
  mediaType?: MediaType;
108
117
  accept?: string[];
118
+ translatable?: boolean;
109
119
  }
110
120
  export interface RelationFieldInput extends FieldInputBase {
111
121
  type: 'relation';
@@ -113,7 +123,7 @@ export interface RelationFieldInput extends FieldInputBase {
113
123
  cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
114
124
  onDelete?: 'set null' | 'cascade' | 'restrict';
115
125
  }
116
- export type BlockFieldInput = Omit<TextFieldInput, 'translatable'> | Omit<RichtextFieldInput, 'translatable'> | NumberFieldInput | BooleanFieldInput | DateFieldInput | EmailFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput;
126
+ export type BlockFieldInput = Omit<TextFieldInput, 'translatable'> | Omit<RichtextFieldInput, 'translatable'> | NumberFieldInput | BooleanFieldInput | DateFieldInput | EmailFieldInput | SelectFieldInput | JsonFieldInput | Omit<MediaFieldInput, 'translatable'>;
117
127
  export interface BlockInput {
118
128
  label: string;
119
129
  fields: Record<string, BlockFieldInput>;
@@ -41,6 +41,17 @@ export function mediaIconFor(type) {
41
41
  export function mediaPublicUrl(baseUrl, key) {
42
42
  return baseUrl ? `${baseUrl.replace(/\/+$/, "")}/${key}` : null;
43
43
  }
44
+ export const DEFAULT_MEDIA_MAX_FILE_SIZE = 10 * 1024 * 1024;
45
+ const FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"];
46
+ export function formatFileSize(bytes) {
47
+ let value = Math.max(0, bytes);
48
+ let unit = 0;
49
+ while (value >= 1024 && unit < FILE_SIZE_UNITS.length - 1) {
50
+ value /= 1024;
51
+ unit++;
52
+ }
53
+ return `${Math.round(value * 10) / 10} ${FILE_SIZE_UNITS[unit]}`;
54
+ }
44
55
  export function slugify(value) {
45
56
  return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036F]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
46
57
  }
@@ -51,7 +62,65 @@ export function normalizeMediaFolder(value) {
51
62
  return segments.length ? segments.join("/") : null;
52
63
  }
53
64
  export function isTranslatableField(field) {
54
- return !!field.translatable && (field.type === "text" || field.type === "richtext");
65
+ return !!field.translatable && (field.type === "text" || field.type === "richtext" || field.type === "media");
66
+ }
67
+ export function isTranslatableMediaField(field) {
68
+ return !!field.translatable && field.type === "media";
69
+ }
70
+ function parseJsonObject(raw) {
71
+ try {
72
+ const parsed = JSON.parse(raw);
73
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
74
+ return parsed;
75
+ }
76
+ } catch {
77
+ return null;
78
+ }
79
+ return null;
80
+ }
81
+ export function decodeTranslatableMedia(value, defaultLocale) {
82
+ if (value == null) return null;
83
+ if (typeof value === "object") return value;
84
+ const raw = String(value).trim();
85
+ if (!raw) return null;
86
+ return (raw.startsWith("{") ? parseJsonObject(raw) : null) ?? { [defaultLocale]: raw };
87
+ }
88
+ export function encodeTranslatableMedia(value) {
89
+ if (value == null) return null;
90
+ if (typeof value === "string") return value.trim() || null;
91
+ if (typeof value !== "object") return null;
92
+ const entries = Object.entries(value).filter(
93
+ (pair) => typeof pair[1] === "string" && pair[1].trim() !== ""
94
+ );
95
+ return entries.length ? JSON.stringify(Object.fromEntries(entries)) : null;
96
+ }
97
+ export function pickTranslatedMedia(values, locale, defaultLocale) {
98
+ if (!values) return null;
99
+ return values[locale] || values[defaultLocale] || Object.values(values).find(Boolean) || null;
100
+ }
101
+ export function translatableMediaKeys(entry) {
102
+ return Object.entries(entry.fields).filter(([, field]) => isTranslatableMediaField(field)).map(([key]) => key);
103
+ }
104
+ export function encodeEntryTranslatableMedia(entry, values) {
105
+ const keys = translatableMediaKeys(entry);
106
+ if (!keys.length) return values;
107
+ const encoded = { ...values };
108
+ for (const key of keys) {
109
+ if (Object.hasOwn(encoded, key)) encoded[key] = encodeTranslatableMedia(encoded[key]);
110
+ }
111
+ return encoded;
112
+ }
113
+ export function decodeEntryTranslatableMedia(entry, rows, defaultLocale) {
114
+ const keys = translatableMediaKeys(entry);
115
+ if (!keys.length) return rows;
116
+ for (const row of rows) {
117
+ for (const key of keys) {
118
+ if (Object.hasOwn(row, key)) {
119
+ row[key] = decodeTranslatableMedia(row[key], defaultLocale);
120
+ }
121
+ }
122
+ }
123
+ return rows;
55
124
  }
56
125
  export function isMultiSelect(field) {
57
126
  return field.type === "select" && !!field.multiple;
@@ -69,7 +69,7 @@ export function buildEntrySchema(entry, i18n, messages) {
69
69
  continue;
70
70
  }
71
71
  if (isTranslatableField(field) && locales.length) {
72
- const record = z.record(z.string(), z.string()).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
72
+ const record = z.record(z.string(), field.type === "media" ? objectKeySchema : z.string()).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
73
73
  shape[key] = field.required ? record.refine((v) => !!v[defaultLocale]?.trim(), m.requiredLocale(defaultLocale)) : record.nullish().transform((v) => v ?? null);
74
74
  continue;
75
75
  }
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default } from './module.mjs'
2
2
 
3
- export { type ModuleOptions } from './module.mjs'
3
+ export { type ModuleOptions, type ModuleOptionsDatabase, type ModuleOptionsMedia } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
5
5
  "license": "MIT",
6
6
  "author": "Edoardo Alberti (https://github.com/xleddyl)",
@@ -60,12 +60,13 @@
60
60
  "dist"
61
61
  ],
62
62
  "dependencies": {
63
+ "@fontsource-variable/hanken-grotesk": "^5.3.0",
64
+ "@fontsource/fragment-mono": "^5.3.0",
63
65
  "@libsql/client": "^0.17.4",
64
66
  "@nuxt/kit": "^4.4.8",
65
67
  "@tailwindcss/vite": "^4.3.2",
66
68
  "@tiptap/starter-kit": "^3.0.0",
67
69
  "@tiptap/vue-3": "^3.0.0",
68
- "tailwindcss": "^4.3.2",
69
70
  "aws4fetch": "^1.0.20",
70
71
  "better-sqlite3": "^12.2.0",
71
72
  "drizzle-kit": "^0.31.0",
@@ -76,6 +77,7 @@
76
77
  "jiti": "^2.7.0",
77
78
  "nuxt-auth-utils": "^0.5.29",
78
79
  "pg": "^8.16.0",
80
+ "tailwindcss": "^4.3.2",
79
81
  "vite-svg-loader": "^5.1.0",
80
82
  "zod": "^4.0.0"
81
83
  },