includio-cms 0.0.34 → 0.0.35

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 (29) hide show
  1. package/dist/admin/components/fields/seo-field.svelte +4 -2
  2. package/dist/admin/components/media/file-upload.svelte +4 -5
  3. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  4. package/dist/admin/components/media/media-library.svelte +7 -1
  5. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  6. package/dist/core/server/fields/resolveImageFields.js +44 -7
  7. package/dist/core/server/generator/fields.js +4 -1
  8. package/dist/core/server/generator/generator.js +1 -1
  9. package/dist/core/server/media/styles/operations/createMediaStyles.d.ts +3 -0
  10. package/dist/core/server/media/styles/operations/createMediaStyles.js +7 -0
  11. package/dist/core/server/media/styles/operations/getImageStyle.d.ts +3 -0
  12. package/dist/core/server/media/styles/operations/getImageStyle.js +8 -0
  13. package/dist/core/server/media/styles/sharp/generateImageStyle.d.ts +3 -0
  14. package/dist/core/server/media/styles/sharp/generateImageStyle.js +16 -0
  15. package/dist/db-postgres/index.js +30 -0
  16. package/dist/db-postgres/schema/imageStyle.d.ts +160 -0
  17. package/dist/db-postgres/schema/imageStyle.js +12 -0
  18. package/dist/db-postgres/schema/index.d.ts +1 -0
  19. package/dist/db-postgres/schema/index.js +1 -0
  20. package/dist/files-local/index.js +12 -25
  21. package/dist/index.d.ts +0 -2
  22. package/dist/index.js +0 -1
  23. package/dist/sveltekit/components/image.svelte +25 -12
  24. package/dist/sveltekit/components/image.svelte.d.ts +3 -2
  25. package/dist/types/adapters/files.d.ts +2 -0
  26. package/dist/types/adapters.d.ts +6 -1
  27. package/dist/types/fields.d.ts +14 -0
  28. package/dist/types/media.d.ts +5 -0
  29. package/package.json +1 -1
@@ -48,7 +48,8 @@
48
48
  type: 'text',
49
49
  slug: 'title',
50
50
  label: 'Title',
51
- required: true
51
+ required: true,
52
+ maxLength: 60
52
53
  };
53
54
 
54
55
  const descriptionField: TextField = {
@@ -56,7 +57,8 @@
56
57
  slug: 'description',
57
58
  label: 'Description',
58
59
  required: false,
59
- multiline: true
60
+ multiline: true,
61
+ maxLength: 160
60
62
  };
61
63
 
62
64
  const keyWordsField: TextField = {
@@ -1,16 +1,14 @@
1
1
  <script lang="ts">
2
- import { getRemotes } from '../../context/remotes.js';
3
2
  import Button from '../../../components/ui/button/button.svelte';
4
3
  import Plus from '@tabler/icons-svelte/icons/plus';
5
4
 
6
- const remotes = getRemotes();
7
-
8
5
  type Props = {
9
6
  folderId?: string | null;
10
7
  accept?: string;
8
+ onUpload?: () => void;
11
9
  };
12
10
 
13
- let { folderId = $bindable(null), accept }: Props = $props();
11
+ let { folderId = $bindable(null), accept, onUpload }: Props = $props();
14
12
 
15
13
  let inputElement: HTMLInputElement;
16
14
 
@@ -30,10 +28,11 @@
30
28
  method: 'POST',
31
29
  body: form
32
30
  });
31
+
32
+ onUpload?.();
33
33
  })
34
34
  );
35
35
 
36
- remotes.getMedia().refresh();
37
36
  input.value = '';
38
37
  }
39
38
  </script>
@@ -1,6 +1,7 @@
1
1
  type Props = {
2
2
  folderId?: string | null;
3
3
  accept?: string;
4
+ onUpload?: () => void;
4
5
  };
5
6
  declare const FileUpload: import("svelte").Component<Props, {}, "folderId">;
6
7
  type FileUpload = ReturnType<typeof FileUpload>;
@@ -78,7 +78,13 @@
78
78
  </Tabs.Root>
79
79
  </div>
80
80
  <div class="flex items-center gap-2">
81
- <FileUpload bind:folderId={selectedFolder} {accept} />
81
+ <FileUpload
82
+ bind:folderId={selectedFolder}
83
+ {accept}
84
+ onUpload={() => {
85
+ fetchFiles(selectedFolder, accept);
86
+ }}
87
+ />
82
88
  </div>
83
89
  </div>
84
90
 
@@ -2,7 +2,7 @@ declare const SidebarInput: import("svelte").Component<(Omit<import("svelte/elem
2
2
  type: "file";
3
3
  files?: FileList;
4
4
  } | {
5
- type?: "number" | "text" | "email" | "checkbox" | "image" | "date" | "radio" | "url" | "hidden" | "color" | "submit" | "reset" | "button" | (string & {}) | "search" | "tel" | "time" | "datetime-local" | "month" | "password" | "range" | "week";
5
+ type?: "number" | "image" | "text" | "date" | "radio" | "url" | "email" | "checkbox" | "hidden" | "color" | "submit" | "reset" | "button" | (string & {}) | "search" | "tel" | "time" | "datetime-local" | "month" | "password" | "range" | "week";
6
6
  files?: undefined;
7
7
  })) & {
8
8
  ref?: HTMLElement | null | undefined;
@@ -1,6 +1,7 @@
1
1
  import { PUBLIC_URL } from '$env/static/public';
2
2
  import { getCMS } from '../../cms.js';
3
3
  import z from 'zod';
4
+ import { getImageStyle } from '../media/styles/operations/getImageStyle.js';
4
5
  export async function resolveMediaFields(data, fields) {
5
6
  const mediaIds = [];
6
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -60,7 +61,7 @@ export async function resolveMediaFields(data, fields) {
60
61
  });
61
62
  const mediaMap = Object.fromEntries(media.map((m) => [m.id, m]));
62
63
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
- const resolveValues = (value, fields) => {
64
+ const resolveValues = async (value, fields) => {
64
65
  // Start with a copy of all original data to preserve non-field properties
65
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
67
  const result = { ...value };
@@ -71,7 +72,42 @@ export async function resolveMediaFields(data, fields) {
71
72
  continue;
72
73
  }
73
74
  switch (field.type) {
74
- case 'image':
75
+ case 'image': {
76
+ if (field.multiple && Array.isArray(val)) {
77
+ result[field.slug] = val.map((id) => ({
78
+ data: mediaMap[id] ?? null,
79
+ styles: {}
80
+ }));
81
+ }
82
+ else {
83
+ if (field.styles) {
84
+ const styles = await Promise.all(field.styles.map(async (style) => {
85
+ const styleDbData = await getImageStyle(val, style);
86
+ return [
87
+ style.name,
88
+ {
89
+ url: styleDbData.url,
90
+ media: style.media,
91
+ mimeType: style.format
92
+ }
93
+ ];
94
+ }));
95
+ const finalVal = {
96
+ data: mediaMap[val] ?? null,
97
+ styles: Object.fromEntries(styles)
98
+ };
99
+ result[field.slug] = finalVal;
100
+ break;
101
+ }
102
+ const finalVal = {
103
+ data: mediaMap[val] ?? null,
104
+ styles: {}
105
+ };
106
+ result[field.slug] = finalVal;
107
+ break;
108
+ }
109
+ break;
110
+ }
75
111
  case 'file': {
76
112
  if (field.multiple && Array.isArray(val)) {
77
113
  result[field.slug] = val.map((id) => mediaMap[id] ?? null);
@@ -98,23 +134,24 @@ export async function resolveMediaFields(data, fields) {
98
134
  case 'object':
99
135
  result[field.slug] = {
100
136
  ...val,
101
- data: resolveValues(val.data, field.fields)
137
+ data: await resolveValues(val.data, field.fields)
102
138
  };
103
139
  break;
104
140
  case 'array':
141
+ result[field.slug] = await Promise.all(
105
142
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
- result[field.slug] = val.map((item) => {
143
+ val.map(async (item) => {
107
144
  // Find the object definition for this item based on its slug
108
145
  const objectDef = field.of.find((objDef) => objDef.slug === item.slug);
109
146
  if (objectDef) {
110
147
  return {
111
148
  ...item,
112
- data: resolveValues(item.data, objectDef.fields)
149
+ data: await resolveValues(item.data, objectDef.fields)
113
150
  };
114
151
  }
115
152
  // If no matching object definition found, return item unchanged
116
153
  return item;
117
- });
154
+ }));
118
155
  break;
119
156
  default:
120
157
  result[field.slug] = val;
@@ -122,5 +159,5 @@ export async function resolveMediaFields(data, fields) {
122
159
  }
123
160
  return result;
124
161
  };
125
- return resolveValues(data, fields);
162
+ return (await resolveValues(data, fields));
126
163
  }
@@ -7,7 +7,10 @@ function getFieldTypeAsString(field) {
7
7
  return 'string';
8
8
  case 'radio':
9
9
  return field.options.map((opt) => `'${opt.value.trim()}'`).join(' | ');
10
- case 'image':
10
+ case 'image': {
11
+ const base = field.multiple ? 'ImageFieldData[]' : 'ImageFieldData';
12
+ return base + (field.required ? '' : ' | null');
13
+ }
11
14
  case 'file': {
12
15
  const base = field.multiple ? 'MediaFile[]' : 'MediaFile';
13
16
  return base + (field.required ? '' : ' | null');
@@ -65,7 +65,7 @@ function generateTypes(config) {
65
65
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
66
66
  const filePath = join(cmsDir, 'types.ts');
67
67
  let code = `// This file is auto-generated. Do not edit directly.\n\n`;
68
- code += `import type { MediaFile } from 'includio-cms/types';\n\n`;
68
+ code += `import type { MediaFile, ImageFieldData } from 'includio-cms/types';\n\n`;
69
69
  if (config.singles && config.singles.length > 0) {
70
70
  code += generateTypesStringForRecords('single', Object.values(config.singles));
71
71
  }
@@ -0,0 +1,3 @@
1
+ import type { ImageFieldStyle } from '../../../../../types/fields.js';
2
+ import type { ImageStyle } from '../../../../../types/media.js';
3
+ export declare function createImageStyle(mediaFileId: string, style: ImageFieldStyle): Promise<ImageStyle>;
@@ -0,0 +1,7 @@
1
+ import { getCMS } from '../../../../cms.js';
2
+ import { generateImageStyle } from '../sharp/generateImageStyle.js';
3
+ export async function createImageStyle(mediaFileId, style) {
4
+ const imageStyleFile = await generateImageStyle(mediaFileId, style);
5
+ const imageStyle = await getCMS().databaseAdapter.createImageStyle(mediaFileId, imageStyleFile, style);
6
+ return imageStyle;
7
+ }
@@ -0,0 +1,3 @@
1
+ import type { ImageFieldStyle } from '../../../../../types/fields.js';
2
+ import type { ImageStyle } from '../../../../../types/media.js';
3
+ export declare function getImageStyle(mediaFileId: string, style: ImageFieldStyle): Promise<ImageStyle>;
@@ -0,0 +1,8 @@
1
+ import { getCMS } from '../../../../cms.js';
2
+ import { createImageStyle } from './createMediaStyles.js';
3
+ export async function getImageStyle(mediaFileId, style) {
4
+ const imageStyle = await getCMS().databaseAdapter.getImageStyle(mediaFileId, style);
5
+ if (imageStyle)
6
+ return imageStyle;
7
+ return createImageStyle(mediaFileId, style);
8
+ }
@@ -0,0 +1,3 @@
1
+ import type { ImageFieldStyle } from '../../../../../types/fields.js';
2
+ import type { UploadedMediaFile } from '../../../../../types/media.js';
3
+ export declare function generateImageStyle(mediaFileId: string, style: ImageFieldStyle): Promise<UploadedMediaFile>;
@@ -0,0 +1,16 @@
1
+ import { getCMS } from '../../../../cms.js';
2
+ import sharp from 'sharp';
3
+ export async function generateImageStyle(mediaFileId, style) {
4
+ const file = await getCMS().filesAdapter.getFile(mediaFileId);
5
+ if (!file) {
6
+ throw new Error('Media file not found');
7
+ }
8
+ const imageBuffer = await file.arrayBuffer();
9
+ const outputBuffer = await sharp(Buffer.from(imageBuffer))
10
+ .resize(style.width, style.height)
11
+ .toFormat(style.format)
12
+ .toBuffer();
13
+ return getCMS().filesAdapter.uploadFile(new File([new Uint8Array(outputBuffer)], `${mediaFileId}_${style.name}.${style.format}`, {
14
+ type: `image/${style.format}`
15
+ }));
16
+ }
@@ -197,6 +197,36 @@ export function pg(config) {
197
197
  .returning()
198
198
  .then(([file]) => file);
199
199
  },
200
+ getImageStyle: async (mediaFileId, style) => {
201
+ const [imageStyle] = await db
202
+ .select()
203
+ .from(schema.imageStylesTable)
204
+ .where(and(eq(schema.imageStylesTable.mediaFileId, mediaFileId), eq(schema.imageStylesTable.name, style.name), eq(schema.imageStylesTable.width, style.width), eq(schema.imageStylesTable.height, style.height)))
205
+ .limit(1);
206
+ if (!imageStyle)
207
+ return null;
208
+ return {
209
+ url: imageStyle.url,
210
+ mimeType: imageStyle.mimeType,
211
+ media: imageStyle.media ? imageStyle.media : undefined
212
+ };
213
+ },
214
+ createImageStyle: async (mediaFileId, file, style) => {
215
+ const [imageStyle] = await db
216
+ .insert(schema.imageStylesTable)
217
+ .values({
218
+ ...style,
219
+ url: file.url,
220
+ mimeType: file.mimeType ?? `image/${style.format}`,
221
+ mediaFileId: mediaFileId
222
+ })
223
+ .returning();
224
+ return {
225
+ url: imageStyle.url,
226
+ mimeType: imageStyle.mimeType,
227
+ media: imageStyle.media ? imageStyle.media : undefined
228
+ };
229
+ },
200
230
  deleteMediaFile: async (id) => {
201
231
  await db.delete(schema.mediaFilesTable).where(eq(schema.mediaFilesTable.id, id));
202
232
  },
@@ -0,0 +1,160 @@
1
+ export declare const imageStylesTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
2
+ name: "image_styles";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
6
+ name: "id";
7
+ tableName: "image_styles";
8
+ dataType: "string";
9
+ columnType: "PgUUID";
10
+ data: string;
11
+ driverParam: string;
12
+ notNull: true;
13
+ hasDefault: true;
14
+ isPrimaryKey: true;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: undefined;
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {}>;
22
+ mediaFileId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
23
+ name: "media_file_id";
24
+ tableName: "image_styles";
25
+ dataType: "string";
26
+ columnType: "PgUUID";
27
+ data: string;
28
+ driverParam: string;
29
+ notNull: true;
30
+ hasDefault: false;
31
+ isPrimaryKey: false;
32
+ isAutoincrement: false;
33
+ hasRuntimeDefault: false;
34
+ enumValues: undefined;
35
+ baseColumn: never;
36
+ identity: undefined;
37
+ generated: undefined;
38
+ }, {}, {}>;
39
+ name: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
40
+ name: "name";
41
+ tableName: "image_styles";
42
+ dataType: "string";
43
+ columnType: "PgText";
44
+ data: string;
45
+ driverParam: string;
46
+ notNull: true;
47
+ hasDefault: false;
48
+ isPrimaryKey: false;
49
+ isAutoincrement: false;
50
+ hasRuntimeDefault: false;
51
+ enumValues: [string, ...string[]];
52
+ baseColumn: never;
53
+ identity: undefined;
54
+ generated: undefined;
55
+ }, {}, {}>;
56
+ url: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
57
+ name: "url";
58
+ tableName: "image_styles";
59
+ dataType: "string";
60
+ columnType: "PgText";
61
+ data: string;
62
+ driverParam: string;
63
+ notNull: true;
64
+ hasDefault: false;
65
+ isPrimaryKey: false;
66
+ isAutoincrement: false;
67
+ hasRuntimeDefault: false;
68
+ enumValues: [string, ...string[]];
69
+ baseColumn: never;
70
+ identity: undefined;
71
+ generated: undefined;
72
+ }, {}, {}>;
73
+ width: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
74
+ name: "width";
75
+ tableName: "image_styles";
76
+ dataType: "number";
77
+ columnType: "PgInteger";
78
+ data: number;
79
+ driverParam: string | number;
80
+ notNull: true;
81
+ hasDefault: false;
82
+ isPrimaryKey: false;
83
+ isAutoincrement: false;
84
+ hasRuntimeDefault: false;
85
+ enumValues: undefined;
86
+ baseColumn: never;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }, {}, {}>;
90
+ height: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
91
+ name: "height";
92
+ tableName: "image_styles";
93
+ dataType: "number";
94
+ columnType: "PgInteger";
95
+ data: number;
96
+ driverParam: string | number;
97
+ notNull: true;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ crop: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
108
+ name: "crop";
109
+ tableName: "image_styles";
110
+ dataType: "boolean";
111
+ columnType: "PgBoolean";
112
+ data: boolean;
113
+ driverParam: boolean;
114
+ notNull: true;
115
+ hasDefault: true;
116
+ isPrimaryKey: false;
117
+ isAutoincrement: false;
118
+ hasRuntimeDefault: false;
119
+ enumValues: undefined;
120
+ baseColumn: never;
121
+ identity: undefined;
122
+ generated: undefined;
123
+ }, {}, {}>;
124
+ media: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
125
+ name: "media";
126
+ tableName: "image_styles";
127
+ dataType: "string";
128
+ columnType: "PgText";
129
+ data: string;
130
+ driverParam: string;
131
+ notNull: false;
132
+ hasDefault: false;
133
+ isPrimaryKey: false;
134
+ isAutoincrement: false;
135
+ hasRuntimeDefault: false;
136
+ enumValues: [string, ...string[]];
137
+ baseColumn: never;
138
+ identity: undefined;
139
+ generated: undefined;
140
+ }, {}, {}>;
141
+ mimeType: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
142
+ name: "mime_type";
143
+ tableName: "image_styles";
144
+ dataType: "string";
145
+ columnType: "PgText";
146
+ data: string;
147
+ driverParam: string;
148
+ notNull: true;
149
+ hasDefault: false;
150
+ isPrimaryKey: false;
151
+ isAutoincrement: false;
152
+ hasRuntimeDefault: false;
153
+ enumValues: [string, ...string[]];
154
+ baseColumn: never;
155
+ identity: undefined;
156
+ generated: undefined;
157
+ }, {}, {}>;
158
+ };
159
+ dialect: "pg";
160
+ }>;
@@ -0,0 +1,12 @@
1
+ import { boolean, integer, pgTable, text, uuid } from 'drizzle-orm/pg-core';
2
+ export const imageStylesTable = pgTable('image_styles', {
3
+ id: uuid('id').primaryKey().defaultRandom(),
4
+ mediaFileId: uuid('media_file_id').notNull(),
5
+ name: text('name').notNull(),
6
+ url: text('url').notNull(),
7
+ width: integer('width').notNull(),
8
+ height: integer('height').notNull(),
9
+ crop: boolean('crop').notNull().default(false),
10
+ media: text('media'),
11
+ mimeType: text('mime_type').notNull()
12
+ });
@@ -5,3 +5,4 @@ export * from './session.js';
5
5
  export * from './user.js';
6
6
  export * from './formSubmission.js';
7
7
  export * from './consentLog.js';
8
+ export * from './imageStyle.js';
@@ -5,3 +5,4 @@ export * from './session.js';
5
5
  export * from './user.js';
6
6
  export * from './formSubmission.js';
7
7
  export * from './consentLog.js';
8
+ export * from './imageStyle.js';
@@ -1,6 +1,6 @@
1
1
  import { extname, resolve } from 'node:path';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { writeFile } from 'node:fs/promises';
3
+ import { writeFile, readFile } from 'node:fs/promises';
4
4
  import sharp from 'sharp';
5
5
  import ffmpeg from 'fluent-ffmpeg';
6
6
  const fullDir = process.env.NODE_ENV === 'production' ? `/data/uploads` : `./static/uploads`;
@@ -53,30 +53,17 @@ async function processVideo(filepath, filename) {
53
53
  }
54
54
  export function local() {
55
55
  return {
56
- // getFiles: async (): Promise<MediaFile[]> => {
57
- // const files = await readdir(dir);
58
- // return await Promise.all(
59
- // files.map(async (file) => {
60
- // const stats = await stat(join(dir, file));
61
- // const ext = extname(file).toLowerCase();
62
- // const type = ['.jpg', '.jpeg', '.png', '.webp'].includes(ext)
63
- // ? 'image'
64
- // : ['.mp4', '.webm'].includes(ext)
65
- // ? 'video'
66
- // : ext === '.pdf'
67
- // ? 'pdf'
68
- // : 'other';
69
- // return {
70
- // id: file,
71
- // name: file,
72
- // url: `/uploads/${file}`,
73
- // type,
74
- // size: stats.size,
75
- // createdAt: stats.birthtime.toISOString()
76
- // };
77
- // })
78
- // );
79
- // },
56
+ getFile: async (id) => {
57
+ const filepath = resolve(fullDir, id);
58
+ try {
59
+ const data = await readFile(filepath);
60
+ const file = new File([data], id);
61
+ return file;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ },
80
67
  uploadFile: async (file) => {
81
68
  const ext = extname(file.name);
82
69
  const id = randomUUID();
package/dist/index.d.ts CHANGED
@@ -2,5 +2,3 @@ export * from './admin/index.js';
2
2
  export type * from './admin/index.js';
3
3
  export * from './core/index.js';
4
4
  export type * from './core/index.js';
5
- export * from './ui/index.js';
6
- export type * from './ui/index.js';
package/dist/index.js CHANGED
@@ -1,3 +1,2 @@
1
1
  export * from './admin/index.js';
2
2
  export * from './core/index.js';
3
- export * from './ui/index.js';
@@ -1,20 +1,33 @@
1
1
  <script lang="ts">
2
- import type { MediaFile } from '../../types/media.js';
2
+ import type { ImageFieldData } from '../../types/fields.js';
3
3
  import type { HTMLImgAttributes } from 'svelte/elements';
4
4
 
5
5
  type Props = HTMLImgAttributes & {
6
- data: MediaFile;
6
+ data: ImageFieldData;
7
+ pictureClass?: string;
7
8
  };
8
9
 
9
- let { class: className = undefined, data, ...restProps }: Props = $props();
10
+ let {
11
+ class: className = undefined,
12
+ pictureClass,
13
+ data,
14
+ loading = 'lazy',
15
+ ...restProps
16
+ }: Props = $props();
10
17
  </script>
11
18
 
12
- <img
13
- src={data.url}
14
- alt={data.alt}
15
- class={className}
16
- width={data.width}
17
- height={data.height}
18
- loading="lazy"
19
- {...restProps}
20
- />
19
+ <picture class={pictureClass}>
20
+ {#each Object.values(data.styles) as style}
21
+ <source srcset={style.url} type={style.mimeType} media={style.media} />
22
+ {/each}
23
+
24
+ <img
25
+ src={data.data.url}
26
+ alt={data.data.alt}
27
+ width={data.data.width}
28
+ height={data.data.height}
29
+ class={className}
30
+ {loading}
31
+ {...restProps}
32
+ />
33
+ </picture>
@@ -1,7 +1,8 @@
1
- import type { MediaFile } from '../../types/media.js';
1
+ import type { ImageFieldData } from '../../types/fields.js';
2
2
  import type { HTMLImgAttributes } from 'svelte/elements';
3
3
  type Props = HTMLImgAttributes & {
4
- data: MediaFile;
4
+ data: ImageFieldData;
5
+ pictureClass?: string;
5
6
  };
6
7
  declare const Image: import("svelte").Component<Props, {}, "">;
7
8
  type Image = ReturnType<typeof Image>;
@@ -1,5 +1,7 @@
1
1
  import type { UploadedMediaFile } from '../media.js';
2
2
  export interface FilesAdapter {
3
+ getFile: GetFile;
3
4
  uploadFile: UploadFile;
4
5
  }
6
+ export type GetFile = (id: string) => Promise<File | null>;
5
7
  export type UploadFile = (file: File) => Promise<UploadedMediaFile>;
@@ -1,7 +1,8 @@
1
1
  import type { ConsentLogData } from './consent.js';
2
2
  import type { RawEntry } from './entries.js';
3
+ import type { ImageFieldStyle } from './fields.js';
3
4
  import type { FormSubmission } from './forms.js';
4
- import type { MediaFile, MediaFolder } from './media.js';
5
+ import type { ImageStyle, MediaFile, MediaFolder, UploadedMediaFile } from './media.js';
5
6
  import type { Session } from './sessions.js';
6
7
  import type { User } from './users.js';
7
8
  export type { AuthAdapter } from './adapters/auth.js';
@@ -19,6 +20,8 @@ export interface DatabaseAdapter {
19
20
  createMediaFolder: CreateMediaFolder;
20
21
  getMediaFolders: GetMediaFolders;
21
22
  createMediaFile: CreateMediaFile;
23
+ getImageStyle: GetImageStyle;
24
+ createImageStyle: CreateImageStyle;
22
25
  getMediaFiles: GetMediaFiles;
23
26
  getMediaFile: GetMediaFile;
24
27
  updateMediaFile: UpdateMediaFile;
@@ -98,6 +101,8 @@ export type UpdateMediaFile = (data: {
98
101
  data: Partial<MediaFile>;
99
102
  }) => Promise<MediaFile>;
100
103
  export type CreateMediaFile = (data: MediaFile) => Promise<MediaFile>;
104
+ export type CreateImageStyle = (mediaFileId: string, file: UploadedMediaFile, style: ImageFieldStyle) => Promise<ImageStyle>;
105
+ export type GetImageStyle = (mediaFileId: string, style: ImageFieldStyle) => Promise<ImageStyle | null>;
101
106
  export interface GetUserOptions {
102
107
  data: {
103
108
  id?: string;
@@ -1,3 +1,4 @@
1
+ import type { ImageStyle, MediaFile } from './media.js';
1
2
  export type FieldType = 'text' | 'richtext' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'image' | 'select' | 'radio' | 'checkboxes' | 'relation' | 'object' | 'array' | 'slug' | 'seo' | 'url';
2
3
  export interface BaseField {
3
4
  slug: string;
@@ -62,6 +63,19 @@ export interface ImageField extends BaseField {
62
63
  aspectRatio?: number;
63
64
  thumbnail?: boolean;
64
65
  defaultValue?: string | string[];
66
+ styles?: ImageFieldStyle[];
67
+ }
68
+ export interface ImageFieldStyle {
69
+ name: string;
70
+ width: number;
71
+ height: number;
72
+ format?: 'jpeg' | 'png' | 'webp' | 'avif';
73
+ media?: string;
74
+ crop?: boolean;
75
+ }
76
+ export interface ImageFieldData {
77
+ data: MediaFile;
78
+ styles: Record<string, ImageStyle>;
65
79
  }
66
80
  export interface SelectField extends BaseField {
67
81
  type: 'select';
@@ -34,3 +34,8 @@ export interface MediaFile {
34
34
  posterUrl: string | null;
35
35
  mimeType: string | null;
36
36
  }
37
+ export interface ImageStyle {
38
+ url: string;
39
+ mimeType: string;
40
+ media?: string;
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",