nextjs-cms 0.7.2 → 0.7.3

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.
@@ -16,41 +16,61 @@ const backgroundSchema = z
16
16
  b: z.number().min(0).max(255).describe('Blue channel (0-255)'),
17
17
  alpha: z.number().min(0).max(1).describe('Alpha channel (0-1)'),
18
18
  })
19
- .describe('Background color for image padding (used when crop is false)');
20
- const sizeSchema = z.strictObject({
19
+ .describe('Background color for image padding (used with fit: contain)');
20
+ /**
21
+ * Size schema — discriminated union of three variants:
22
+ * - strict: true — user must upload exact dimensions, no server resize
23
+ * - fit: 'cover' — accept any dimensions, crop to fill (sharp fit: cover)
24
+ * - fit: 'contain' — accept any dimensions, pad with background (sharp fit: contain)
25
+ */
26
+ const strictSizeSchema = z.strictObject({
21
27
  width: z.number().describe('Image width in pixels'),
22
28
  height: z.number().describe('Image height in pixels'),
23
- /**
24
- * @true The image will not have dimensions constraints,
25
- * and will be cropped to fit the specified dimensions
26
- *
27
- * @false The user will be forced to upload an image with the specified dimensions
28
- *
29
- * @example
30
- * size: {
31
- * width: 1200,
32
- * height: 450,
33
- * crop: true, // No constraints
34
- * }
35
- */
36
- crop: z.boolean().describe('Whether to crop the image to fit dimensions'),
37
- /**
38
- * Background color for padding when crop is false.
39
- * Falls back to CMS config media.images.background if not provided.
40
- */
41
- background: backgroundSchema.optional().describe('Background color for padding when not cropping'),
29
+ strict: z.literal(true).describe('Require exact upload dimensions, no server resize'),
30
+ quality: z.number().min(1).max(100).optional().describe('Output quality (1-100)'),
31
+ });
32
+ const coverSizeSchema = z.strictObject({
33
+ width: z.number().describe('Image width in pixels'),
34
+ height: z.number().describe('Image height in pixels'),
35
+ fit: z.literal('cover').describe('Crop to fill dimensions (sharp fit: cover)'),
36
+ quality: z.number().min(1).max(100).optional().describe('Output quality (1-100)'),
42
37
  });
43
- const thumbnailSchema = z.strictObject({
44
- width: z.number().describe('Thumbnail width in pixels'),
45
- height: z.number().describe('Thumbnail height in pixels'),
46
- crop: z.boolean().describe('Whether to crop the thumbnail'),
47
- quality: z.number().optional().describe('Thumbnail quality (0-100)'),
38
+ const containSizeSchema = z.strictObject({
39
+ width: z.number().describe('Image width in pixels'),
40
+ height: z.number().describe('Image height in pixels'),
41
+ fit: z.literal('contain').describe('Pad with background to fill dimensions (sharp fit: contain)'),
42
+ quality: z.number().min(1).max(100).optional().describe('Output quality (1-100)'),
48
43
  /**
49
- * Background color for padding when crop is false.
44
+ * Background color for padding.
50
45
  * Falls back to CMS config media.images.background if not provided.
51
46
  */
52
- background: backgroundSchema.optional().describe('Background color for padding when not cropping'),
47
+ background: backgroundSchema.optional().describe('Background color for padding'),
53
48
  });
49
+ const sizeSchema = z.union([strictSizeSchema, z.discriminatedUnion('fit', [coverSizeSchema, containSizeSchema])]);
50
+ /**
51
+ * Thumbnail schema — discriminated union:
52
+ * - fit: 'cover' — crop thumbnail to fill
53
+ * - fit: 'contain' — pad thumbnail with background
54
+ */
55
+ const thumbnailSchema = z.discriminatedUnion('fit', [
56
+ z.strictObject({
57
+ width: z.number().describe('Thumbnail width in pixels'),
58
+ height: z.number().describe('Thumbnail height in pixels'),
59
+ fit: z.literal('cover').describe('Crop thumbnail to fill'),
60
+ quality: z.number().min(1).max(100).optional().describe('Thumbnail quality (1-100)'),
61
+ }),
62
+ z.strictObject({
63
+ width: z.number().describe('Thumbnail width in pixels'),
64
+ height: z.number().describe('Thumbnail height in pixels'),
65
+ fit: z.literal('contain').describe('Pad thumbnail with background'),
66
+ quality: z.number().min(1).max(100).optional().describe('Thumbnail quality (1-100)'),
67
+ /**
68
+ * Background color for padding.
69
+ * Falls back to CMS config media.images.background if not provided.
70
+ */
71
+ background: backgroundSchema.optional().describe('Background color for padding'),
72
+ }),
73
+ ]);
54
74
  const maxFileSizeSchema = z.strictObject({
55
75
  size: z.number().describe('Maximum file size'),
56
76
  unit: z.enum(['kb', 'mb']).describe('Size unit'),
@@ -152,9 +172,13 @@ export class PhotoField extends FileField {
152
172
  this._thumbnail = this._thumbnailConfig;
153
173
  }
154
174
  else {
155
- // Always fetch fresh CMS config (getCMSConfig() is already cached with version checking)
175
+ // Fetch CMS config and construct a ThumbnailConfig from it
156
176
  const cmsConfig = await getCMSConfig();
157
- this._thumbnail = cmsConfig.media.images.thumbnail;
177
+ const t = cmsConfig.media.images.thumbnail;
178
+ const fit = t.fit ?? 'contain';
179
+ this._thumbnail = fit === 'contain'
180
+ ? { width: t.width, height: t.height, fit: 'contain', quality: t.quality }
181
+ : { width: t.width, height: t.height, fit: 'cover', quality: t.quality };
158
182
  }
159
183
  return this._thumbnail;
160
184
  }
@@ -206,55 +230,53 @@ export class PhotoField extends FileField {
206
230
  fs.mkdirSync(thumbsFolder, { recursive: true });
207
231
  }
208
232
  /**
209
- * Check if the image needs to be resized and write the file to disk
210
- * - crop: true -> fit: 'cover' (crops to fill dimensions)
211
- * - crop: false -> fit: 'contain' with background (preserves all content, adds padding)
233
+ * Write the main image to disk
234
+ * - strict: true -> no resize, just convert to webp
235
+ * - fit: 'cover' -> crop to fill dimensions
236
+ * - fit: 'contain' -> pad with background to fill dimensions
212
237
  */
238
+ const cmsImageQuality = cmsConfig.media.images.image.quality ?? 80;
213
239
  if (this.size) {
214
- if (this.size.crop) {
215
- // Crop mode: use 'cover' to fill dimensions by cropping excess
240
+ const quality = this.size.quality ?? cmsImageQuality;
241
+ const outputPath = path.join(uploadsFolder, '.photos', this._folder, this.value);
242
+ if ('strict' in this.size) {
243
+ // Strict mode: exact dimensions already validated, just convert to webp
244
+ await this._sharpImage.clone().webp({ quality }).toFile(outputPath);
245
+ }
246
+ else if (this.size.fit === 'cover') {
216
247
  await this._sharpImage
217
248
  .clone()
218
- .resize({
219
- width: this.size.width,
220
- height: this.size.height,
221
- fit: 'cover',
222
- })
223
- .webp()
224
- .toFile(path.join(uploadsFolder, '.photos', this._folder, this.value));
249
+ .resize({ width: this.size.width, height: this.size.height, fit: 'cover' })
250
+ .webp({ quality })
251
+ .toFile(outputPath);
225
252
  }
226
253
  else {
227
- // Contain mode: preserve all content with background padding
228
- // Use field-level background, fall back to CMS config background
254
+ // contain: pad with background
255
+ const background = this.size.background ?? cmsBackground;
229
256
  await this._sharpImage
230
257
  .clone()
231
- .resize({
232
- width: this.size.width,
233
- height: this.size.height,
234
- fit: 'contain',
235
- background: this.size.background ?? cmsBackground,
236
- })
237
- .webp()
238
- .toFile(path.join(uploadsFolder, '.photos', this._folder, this.value));
258
+ .resize({ width: this.size.width, height: this.size.height, fit: 'contain', background })
259
+ .webp({ quality })
260
+ .toFile(outputPath);
239
261
  }
240
262
  }
241
263
  else {
242
264
  await this._sharpImage.clone().toFile(path.join(uploadsFolder, '.photos', this._folder, this.value));
243
265
  }
244
266
  /**
245
- * Also, write a thumbnail
246
- * Use CMS config background for padding when crop is false
267
+ * Write a thumbnail
247
268
  */
269
+ const thumbBackground = ('background' in thumbnail ? thumbnail.background : undefined) ?? cmsBackground;
248
270
  await this._sharpImage
249
271
  .clone()
250
272
  .resize({
251
273
  width: thumbnail.width,
252
274
  height: thumbnail.height,
253
- fit: thumbnail.crop ? 'cover' : 'contain',
254
- background: thumbnail.background ?? cmsBackground,
275
+ fit: thumbnail.fit,
276
+ ...(thumbnail.fit === 'contain' ? { background: thumbBackground } : {}),
255
277
  })
256
278
  .webp({
257
- quality: thumbnail.quality ?? 80,
279
+ quality: thumbnail.quality ?? cmsImageQuality,
258
280
  })
259
281
  .toFile(path.join(uploadsFolder, '.thumbs', this._folder, this.value));
260
282
  }
@@ -416,22 +438,11 @@ export class PhotoField extends FileField {
416
438
  throw new Error(getString('fileCorrupted', this.locale));
417
439
  }
418
440
  /**
419
- * Check the size
441
+ * Check the size — only enforce exact dimensions when strict mode is set
420
442
  */
421
- if (this.size) {
422
- /**
423
- * Check if the image does not need to be cropped (dimensions are constrained)
424
- */
425
- if (!this.size.crop) {
426
- /**
427
- * Just resize the image?
428
- */
429
- /**
430
- * Check if the size matches the required size
431
- */
432
- if (metadata.width !== this.size.width || metadata.height !== this.size.height) {
433
- throw new Error(getString('imageDimensionMismatchDetailed', this.locale, { field: this.getLocalizedLabel(), actual: `${metadata.width}x${metadata.height}`, required: `${this.size.width}x${this.size.height}` }));
434
- }
443
+ if (this.size && 'strict' in this.size) {
444
+ if (metadata.width !== this.size.width || metadata.height !== this.size.height) {
445
+ throw new Error(getString('imageDimensionMismatchDetailed', this.locale, { field: this.getLocalizedLabel(), actual: `${metadata.width}x${metadata.height}`, required: `${this.size.width}x${this.size.height}` }));
435
446
  }
436
447
  }
437
448
  /**
@@ -31,10 +31,10 @@ declare const allowImageUploadsSchema: z.ZodObject<{
31
31
  */
32
32
  omitExtension: z.ZodOptional<z.ZodBoolean>;
33
33
  format: z.ZodOptional<z.ZodEnum<{
34
- webp: "webp";
35
- jpg: "jpg";
36
34
  jpeg: "jpeg";
35
+ jpg: "jpg";
37
36
  png: "png";
37
+ webp: "webp";
38
38
  }>>;
39
39
  }, z.core.$strict>;
40
40
  declare const configSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -72,10 +72,10 @@ declare const configSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
72
72
  */
73
73
  omitExtension: z.ZodOptional<z.ZodBoolean>;
74
74
  format: z.ZodOptional<z.ZodEnum<{
75
- webp: "webp";
76
- jpg: "jpg";
77
75
  jpeg: "jpeg";
76
+ jpg: "jpg";
78
77
  png: "png";
78
+ webp: "webp";
79
79
  }>>;
80
80
  }, z.core.$strict>>;
81
81
  }, z.core.$strict>, z.ZodObject<{
@@ -118,7 +118,7 @@ export declare class RichTextField extends Field<'rich_text', Config> {
118
118
  } | undefined;
119
119
  handleMethod?: "base64" | "tempSave" | undefined;
120
120
  omitExtension?: boolean | undefined;
121
- format?: "webp" | "jpg" | "jpeg" | "png" | undefined;
121
+ format?: "jpeg" | "jpg" | "png" | "webp" | undefined;
122
122
  };
123
123
  sanitize: boolean;
124
124
  type: "rich_text";
@@ -186,10 +186,10 @@ declare const optionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
186
186
  */
187
187
  omitExtension: z.ZodOptional<z.ZodBoolean>;
188
188
  format: z.ZodOptional<z.ZodEnum<{
189
- webp: "webp";
190
- jpg: "jpg";
191
189
  jpeg: "jpeg";
190
+ jpg: "jpg";
192
191
  png: "png";
192
+ webp: "webp";
193
193
  }>>;
194
194
  }, z.core.$strict>>;
195
195
  name: z.ZodString;
@@ -248,10 +248,10 @@ declare const richTextFieldConfigSchema: z.ZodIntersection<z.ZodDiscriminatedUni
248
248
  */
249
249
  omitExtension: z.ZodOptional<z.ZodBoolean>;
250
250
  format: z.ZodOptional<z.ZodEnum<{
251
- webp: "webp";
252
- jpg: "jpg";
253
251
  jpeg: "jpeg";
252
+ jpg: "jpg";
254
253
  png: "png";
254
+ webp: "webp";
255
255
  }>>;
256
256
  }, z.core.$strict>>;
257
257
  name: z.ZodString;
@@ -346,8 +346,8 @@ declare const selectFieldConfigSchema: z.ZodIntersection<z.ZodUnion<readonly [z.
346
346
  }, z.core.$strict>]>, z.ZodObject<{
347
347
  type: z.ZodLiteral<"select">;
348
348
  optionsType: z.ZodEnum<{
349
- section: "section";
350
349
  db: "db";
350
+ section: "section";
351
351
  static: "static";
352
352
  }>;
353
353
  build: z.ZodFunction<z.core.$ZodFunctionArgs, z.ZodCustom<SelectField, SelectField>>;
@@ -118,7 +118,9 @@ declare const optionsSchema: z.ZodObject<{
118
118
  */
119
119
  allowRecursiveDelete: z.ZodOptional<z.ZodBoolean>;
120
120
  fields: z.ZodUnion<[z.ZodArray<z.ZodCustom<FieldConfig, FieldConfig>>, z.ZodArray<z.ZodCustom<FieldGroupConfig, FieldGroupConfig>>]>;
121
+ readonly: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
121
122
  name: z.ZodString;
123
+ order: z.ZodNumber;
122
124
  db: z.ZodObject<{
123
125
  table: z.ZodString;
124
126
  identifier: z.ZodOptional<z.ZodCustom<FieldConfig, FieldConfig>>;
@@ -142,8 +144,6 @@ declare const optionsSchema: z.ZodObject<{
142
144
  name: z.ZodOptional<z.ZodString>;
143
145
  }, z.core.$strict>>>;
144
146
  }, z.core.$strict>;
145
- readonly: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
146
- order: z.ZodNumber;
147
147
  icon: z.ZodOptional<z.ZodString>;
148
148
  gallery: z.ZodOptional<z.ZodObject<{
149
149
  db: z.ZodObject<{
@@ -156,7 +156,10 @@ declare const optionsSchema: z.ZodObject<{
156
156
  thumbnail: z.ZodOptional<z.ZodObject<{
157
157
  width: z.ZodNumber;
158
158
  height: z.ZodNumber;
159
- crop: z.ZodBoolean;
159
+ fit: z.ZodEnum<{
160
+ cover: "cover";
161
+ contain: "contain";
162
+ }>;
160
163
  quality: z.ZodNumber;
161
164
  }, z.core.$strict>>;
162
165
  }, z.core.$strict>>;
@@ -215,7 +218,9 @@ export declare const categorySectionConfigSchema: z.ZodObject<{
215
218
  * @default false
216
219
  */
217
220
  allowRecursiveDelete: z.ZodOptional<z.ZodBoolean>;
221
+ readonly: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
218
222
  name: z.ZodString;
223
+ order: z.ZodNumber;
219
224
  db: z.ZodObject<{
220
225
  table: z.ZodString;
221
226
  identifier: z.ZodOptional<z.ZodCustom<FieldConfig, FieldConfig>>;
@@ -239,8 +244,6 @@ export declare const categorySectionConfigSchema: z.ZodObject<{
239
244
  name: z.ZodOptional<z.ZodString>;
240
245
  }, z.core.$strict>>>;
241
246
  }, z.core.$strict>;
242
- readonly: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
243
- order: z.ZodNumber;
244
247
  icon: z.ZodOptional<z.ZodString>;
245
248
  gallery: z.ZodOptional<z.ZodObject<{
246
249
  db: z.ZodObject<{
@@ -253,7 +256,10 @@ export declare const categorySectionConfigSchema: z.ZodObject<{
253
256
  thumbnail: z.ZodOptional<z.ZodObject<{
254
257
  width: z.ZodNumber;
255
258
  height: z.ZodNumber;
256
- crop: z.ZodBoolean;
259
+ fit: z.ZodEnum<{
260
+ cover: "cover";
261
+ contain: "contain";
262
+ }>;
257
263
  quality: z.ZodNumber;
258
264
  }, z.core.$strict>>;
259
265
  }, z.core.$strict>>;
@@ -1 +1 @@
1
- {"version":3,"file":"category.d.ts","sourceRoot":"","sources":["../../../src/core/sections/category.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAChE,OAAO,EACH,OAAO,EAKV,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAA;AAEzE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAExB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAqBd;;;;OAIG;;IAEH;;;;;OAKG;;kBAGL,CAAA;AAEF,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAE1C,qBAAa,eAAgB,SAAQ,OAAO,CAAC,MAAM,CAAC;IAChD,gBAAyB,CAAC,UAAU,CAAC,qBAAoB;IACzD,SAAkB,IAAI,cAAa;IACnC,SAAkB,KAAK,EAAE;QACrB,OAAO,EAAE,eAAe,CAAA;QACxB,QAAQ,EAAE,eAAe,CAAA;QACzB,MAAM,EAAE,eAAe,CAAA;KAC1B,CAAA;IACD,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAI;IAC1B,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAA;gBAE1B,MAAM,EAAE,kBAAkB,CAAC,MAAM,CAAC;CA2CjD;AAED,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA3Ef;;;;OAIG;;IAEH;;;;;OAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmEL,CAAA;AAOF,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IArFpC;;;;OAIG;;IAEH;;;;;OAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkFL,CAAA;AAEF,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAElE;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAE/E;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAmDtF"}
1
+ {"version":3,"file":"category.d.ts","sourceRoot":"","sources":["../../../src/core/sections/category.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAChE,OAAO,EACH,OAAO,EAKV,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAA;AAEzE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAExB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAqBd;;;;OAIG;;IAEH;;;;;OAKG;;kBAGL,CAAA;AAEF,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAE1C,qBAAa,eAAgB,SAAQ,OAAO,CAAC,MAAM,CAAC;IAChD,gBAAyB,CAAC,UAAU,CAAC,qBAAoB;IACzD,SAAkB,IAAI,cAAa;IACnC,SAAkB,KAAK,EAAE;QACrB,OAAO,EAAE,eAAe,CAAA;QACxB,QAAQ,EAAE,eAAe,CAAA;QACzB,MAAM,EAAE,eAAe,CAAA;KAC1B,CAAA;IACD,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAI;IAC1B,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAA;gBAE1B,MAAM,EAAE,kBAAkB,CAAC,MAAM,CAAC;CA2CjD;AAED,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA3Ef;;;;OAIG;;IAEH;;;;;OAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmEL,CAAA;AAOF,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IArFpC;;;;OAIG;;IAEH;;;;;OAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkFL,CAAA;AAEF,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAElE;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAE/E;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAmDtF"}