@limetech/lime-elements 39.42.0 → 39.42.2

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [39.42.2](https://github.com/Lundalogik/lime-elements/compare/v39.42.1...v39.42.2) (2026-07-10)
2
+
3
+ ### Bug Fixes
4
+
5
+
6
+ * **text-editor:** size inline images given bare-pixel dimensions ([713ae95](https://github.com/Lundalogik/lime-elements/commit/713ae9565897961ed3572ebe912b3f6012f616df))
7
+
8
+ ## [39.42.1](https://github.com/Lundalogik/lime-elements/compare/v39.42.0...v39.42.1) (2026-07-09)
9
+
10
+ ### Bug Fixes
11
+
12
+
13
+ * **file:** remove baked-in defaults from the image-resize util ([7a5d29d](https://github.com/Lundalogik/lime-elements/commit/7a5d29d21a9a1ff4cf8e0946993cc7e2a4f22258))
14
+
1
15
  ## [39.42.0](https://github.com/Lundalogik/lime-elements/compare/v39.41.0...v39.42.0) (2026-07-08)
2
16
 
3
17
  ### Features
@@ -14,15 +14,17 @@
14
14
  *
15
15
  * Why resize client-side?
16
16
  * - Faster perceived uploads and lower bandwidth usage
17
- * - Consistent avatar sizes and formats (e.g., JPEG 400x400)
17
+ * - Consistent output sizes and formats (e.g., a 400x400 thumbnail)
18
18
  * - No server-side transformation required for common cases
19
19
  *
20
20
  * Fit strategies
21
- * - `cover` (default): The image is scaled to cover the target rectangle, and
22
- * the excess parts are center-cropped. Good for avatars.
21
+ * - `cover`: The image is scaled to cover the target rectangle, and the excess
22
+ * parts are center-cropped. Good for avatars.
23
23
  * - `contain`: The image is scaled to fit entirely within the target rectangle
24
24
  * without cropping, letterboxing if needed. Good when you must preserve the
25
25
  * entire image.
26
+ * - omitted: scales the whole image to fit without cropping (no pixels are
27
+ * discarded).
26
28
  *
27
29
  * Decoding & EXIF orientation
28
30
  * EXIF orientation is a piece of metadata stored inside image files
@@ -48,9 +50,11 @@
48
50
  * library or WASM module; this utility intentionally avoids extra dependencies.
49
51
  *
50
52
  * Output type & quality
51
- * - Default output is `image/jpeg` with `quality=0.85`, which is typically
52
- * appropriate for avatars. You can switch to `image/png` to preserve
53
- * transparency.
53
+ * - When `type` is omitted the source format is kept if the canvas can encode
54
+ * it: a PNG stays a PNG (preserving transparency) and everything else is
55
+ * encoded as `image/jpeg`. Set `type` explicitly to force a format.
56
+ * - JPEG `quality` is optional; when omitted the browser's native encoding
57
+ * quality is used rather than an imposed value.
54
58
  * - The output filename extension is adjusted to match the chosen MIME type by
55
59
  * default (e.g., `.jpg` or `.png`). You can override naming via the `rename`
56
60
  * option.
@@ -75,9 +79,9 @@
75
79
  * const processed = await resizeImage(file, {
76
80
  * width: 400,
77
81
  * height: 400,
78
- * fit: 'cover', // default; center-crops
79
- * type: 'image/jpeg', // default
80
- * quality: 0.85, // default
82
+ * fit: 'cover', // center-crops; omit to fit without cropping
83
+ * type: 'image/jpeg', // omit to keep the source format
84
+ * quality: 0.85, // omit to use the browser's native quality
81
85
  * });
82
86
  * // Upload `processed` instead of the original file
83
87
  * ```
@@ -106,6 +110,17 @@
106
110
  * ```
107
111
  */
108
112
  // (Removed exported ResizeFit to avoid forcing a public symbol.)
113
+ /**
114
+ * The image formats the canvas can reliably encode, mapped to their filename
115
+ * extension. Single source of truth for what this util can output: both the
116
+ * output-type resolution and the output filename extension derive from it. The
117
+ * `ResizeOptions.type` union below mirrors these keys — keep the two in sync
118
+ * when adding a format.
119
+ */
120
+ const ENCODABLE_TYPES = {
121
+ 'image/jpeg': 'jpg',
122
+ 'image/png': 'png',
123
+ };
109
124
  /**
110
125
  * Resize an image file on the client using Canvas/OffscreenCanvas.
111
126
  * Returns a new File with the requested format and dimensions.
@@ -120,7 +135,17 @@
120
135
  * @param options - Configuration for the resize operation.
121
136
  */
122
137
  async function resizeImage(file, options) {
123
- const { fit = 'cover', type = 'image/jpeg', quality = 0.85, rename = (name) => renameWithType(name, type), } = options;
138
+ var _a;
139
+ // This shared utility carries no opinionated defaults: an omitted option
140
+ // preserves the source file's own property rather than imposing a value.
141
+ // Consumers that want a specific default (e.g. limel-profile-picture) apply
142
+ // it themselves. See the `ResizeOptions` doc for the per-option behavior.
143
+ const { fit, quality } = options;
144
+ // When no output type is requested, keep the source file's format if the
145
+ // canvas can encode it (PNG stays PNG, preserving transparency); anything
146
+ // else falls back to JPEG.
147
+ const type = resolveOutputType(file.type, options.type);
148
+ const rename = (_a = options.rename) !== null && _a !== void 0 ? _a : ((name) => renameWithType(name, type));
124
149
  const source = await loadSource(file);
125
150
  const sourceWidth = source.width;
126
151
  const sourceHeight = source.height;
@@ -209,7 +234,7 @@ function get2dContext(canvas) {
209
234
  * Convert the canvas content to a Blob, supporting both canvas types.
210
235
  * @param canvas - The source canvas
211
236
  * @param type - Output MIME type
212
- * @param quality - JPEG quality (0..1)
237
+ * @param quality - JPEG quality (0..1); omit to use the browser default
213
238
  */
214
239
  function canvasToBlob(canvas, type, quality) {
215
240
  if ('convertToBlob' in canvas) {
@@ -295,7 +320,8 @@ async function loadImageElement(file) {
295
320
  * @param sh - Source height
296
321
  * @param tw - Target width
297
322
  * @param th - Target height
298
- * @param fit - Fit mode (cover/contain)
323
+ * @param fit - Fit mode; `cover` center-crops, anything else (including
324
+ * omitted) scales the whole image to fit without cropping
299
325
  */
300
326
  function computeRects(sw, sh, tw, th, fit) {
301
327
  const sRatio = sw / sh;
@@ -333,13 +359,30 @@ function computeRects(sw, sh, tw, th, fit) {
333
359
  const dy = (th - drawH) / 2;
334
360
  return { sx: 0, sy: 0, sw, sh, dx, dy, dw: drawW, dh: drawH };
335
361
  }
362
+ /**
363
+ * Resolve the output MIME type. An explicit request wins; otherwise the source
364
+ * file's format is kept when the canvas can encode it (see `ENCODABLE_TYPES`),
365
+ * so a PNG stays a PNG (preserving transparency) while every other input
366
+ * (WebP, HEIC, …) falls back to JPEG.
367
+ *
368
+ * @param sourceType - MIME type of the input file
369
+ * @param requested - Explicitly requested output type, if any
370
+ */
371
+ function resolveOutputType(sourceType, requested) {
372
+ if (requested) {
373
+ return requested;
374
+ }
375
+ // Keep the source format when the canvas can encode it; otherwise JPEG.
376
+ const canEncodeSource = Object.prototype.hasOwnProperty.call(ENCODABLE_TYPES, sourceType);
377
+ return canEncodeSource ? sourceType : 'image/jpeg';
378
+ }
336
379
  /**
337
380
  * Update filename extension to match the desired MIME type.
338
381
  * @param name - Original filename
339
382
  * @param type - Output MIME type
340
383
  */
341
384
  function renameWithType(name, type) {
342
- const ext = type === 'image/png' ? 'png' : 'jpg';
385
+ const ext = ENCODABLE_TYPES[type];
343
386
  const idx = name.lastIndexOf('.');
344
387
  const base = idx > 0 ? name.slice(0, idx) : name;
345
388
  return `${base}.${ext}`;
@@ -3,7 +3,7 @@
3
3
  var config = require('./config-dit--4m5.js');
4
4
  var layout = require('./layout-CL_D-afg.js');
5
5
  var types = require('./types-C7gNcwzE.js');
6
- var imageResize = require('./image-resize-BWRGvgkM.js');
6
+ var imageResize = require('./image-resize-CG2fitJT.js');
7
7
  var dispatchResizeEvent = require('./dispatch-resize-event-DH5CaB3p.js');
8
8
 
9
9
  /**
@@ -4,7 +4,7 @@ var index = require('./index-DYg_7kkT.js');
4
4
  var translations = require('./translations-Bnw00bkf.js');
5
5
  var fileMetadata = require('./file-metadata-BrhOssGG.js');
6
6
  var formatBytes = require('./format-bytes-CpUKKU86.js');
7
- var imageResize = require('./image-resize-BWRGvgkM.js');
7
+ var imageResize = require('./image-resize-CG2fitJT.js');
8
8
  require('./icons-isR5V1X0.js');
9
9
  require('./get-icon-props-CwpDdQDI.js');
10
10
 
@@ -5,13 +5,20 @@ var files = require('./files-Bt11HLi4.js');
5
5
  var getIconProps = require('./get-icon-props-CwpDdQDI.js');
6
6
  var translations = require('./translations-Bnw00bkf.js');
7
7
  var randomString = require('./random-string-BTzDB2ee.js');
8
- var imageResize = require('./image-resize-BWRGvgkM.js');
8
+ var imageResize = require('./image-resize-CG2fitJT.js');
9
9
  var image_template = require('./image.template-DZC6_5a4.js');
10
10
  require('./file-metadata-BrhOssGG.js');
11
11
  require('./icons-isR5V1X0.js');
12
12
 
13
13
  const profilePictureCss = () => `@charset "UTF-8";:host(limel-profile-picture){position:relative;display:inline-flex;min-width:1.5rem;min-height:1.5rem;border-radius:var(--profile-picture-border-radius, 100vw);background-color:rgb(var(--contrast-400))}*{box-sizing:border-box}limel-file-dropzone,limel-file-input,button.avatar{display:flex;align-items:center;justify-content:center;width:100%;height:100%}button{appearance:none;background:none;border:none;padding:0;margin:0;font:inherit;color:inherit;text-align:inherit;display:block}button:focus{outline:none}button:focus-visible{outline:none;box-shadow:var(--shadow-depth-8-focused)}button.avatar{overflow:hidden;border-radius:var(--profile-picture-border-radius, 100vw)}:host(:not([disabled]):not([disabled=true])) button.avatar{transition:color var(--limel-clickable-transition-speed, 0.4s) ease, background-color var(--limel-clickable-transition-speed, 0.4s) ease, box-shadow var(--limel-clickable-transform-speed, 0.4s) ease, transform var(--limel-clickable-transform-speed, 0.4s) var(--limel-clickable-transform-timing-function, ease);cursor:pointer;color:var(--limel-theme-on-surface-color);background-color:transparent}:host(:not([disabled]):not([disabled=true])) button.avatar:hover,:host(:not([disabled]):not([disabled=true])) button.avatar:focus,:host(:not([disabled]):not([disabled=true])) button.avatar:focus-visible{will-change:color, background-color, box-shadow, transform}:host(:not([disabled]):not([disabled=true])) button.avatar:hover,:host(:not([disabled]):not([disabled=true])) button.avatar:focus-visible{transform:translate3d(0, 0.01rem, 0);color:var(--limel-theme-on-surface-color);background-color:var(--lime-elevated-surface-background-color)}:host(:not([disabled]):not([disabled=true])) button.avatar:hover{box-shadow:var(--button-shadow-hovered)}:host(:not([disabled]):not([disabled=true])) button.avatar:active{--limel-clickable-transform-timing-function:cubic-bezier( 0.83, -0.15, 0.49, 1.16 );transform:translate3d(0, 0.05rem, 0);box-shadow:var(--button-shadow-pressed)}:host(:not([disabled]):not([disabled=true])) button.avatar:hover,:host(:not([disabled]):not([disabled=true])) button.avatar:active{--limel-clickable-transition-speed:0.2s;--limel-clickable-transform-speed:0.16s}:host([invalid]:not([invalid=false])) button.avatar{box-shadow:var(--shadow-error-state)}button.remove{transition:color var(--limel-clickable-transition-speed, 0.4s) ease, background-color var(--limel-clickable-transition-speed, 0.4s) ease, box-shadow var(--limel-clickable-transform-speed, 0.4s) ease, transform var(--limel-clickable-transform-speed, 0.4s) var(--limel-clickable-transform-timing-function, ease);cursor:pointer;color:var(--limel-theme-on-surface-color);background-color:rgb(var(--contrast-900))}button.remove:hover,button.remove:focus,button.remove:focus-visible{will-change:color, background-color, box-shadow, transform}button.remove:hover,button.remove:focus-visible{transform:translate3d(0, 0.01rem, 0);color:rgb(var(--color-white));background-color:rgb(var(--color-red-default))}button.remove:hover{box-shadow:var(--button-shadow-hovered)}button.remove:active{--limel-clickable-transform-timing-function:cubic-bezier( 0.83, -0.15, 0.49, 1.16 );transform:translate3d(0, 0.05rem, 0);box-shadow:var(--button-shadow-pressed)}button.remove:hover,button.remove:active{--limel-clickable-transition-speed:0.2s;--limel-clickable-transform-speed:0.16s}button.remove{cursor:pointer;height:1.25rem;width:1.25rem;border-radius:50%;background-repeat:no-repeat;background-position:center;background-size:0.75rem;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3E%3Cdefs/%3E%3Cpath%20fill='rgb(255,255,255)'%20d='M7.219%205.781L5.78%207.22%2014.563%2016%205.78%2024.781%207.22%2026.22%2016%2017.437l8.781%208.782%201.438-1.438L17.437%2016l8.782-8.781L24.78%205.78%2016%2014.563z'/%3E%3C/svg%3E");position:absolute;top:0;left:0;opacity:0}:host(:hover) button.remove,:host(:focus) button.remove,:host(:focus-visible) button.remove,:host(:focus-within) button.remove,:host(:active) button.remove{animation:show 0.4s ease-in-out forwards}@keyframes show{0%{transform:scale(0.9);opacity:0}100%{transform:scale(1);opacity:1}}button.avatar,img,limel-icon{border-radius:var(--profile-picture-border-radius, 100vw)}limel-icon{width:calc(100% - 1rem);min-width:1rem;max-width:4rem;color:var(--limel-theme-text-secondary-on-background-color);margin:auto}img{object-fit:var(--limel-profile-picture-object-fit);width:100%;height:100%}:host(.has-image-error) img{border:1px dashed rgb(var(--contrast-600));background:url("data:image/svg+xml;charset=utf-8, <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' style='fill-rule:evenodd;'><path fill='rgba(186,186,192,0.16)' d='M0 0h4v4H0zM4 4h4v4H4z'/></svg>");background-size:0.5rem}limel-spinner{position:absolute;inset:0;margin:auto}limel-popover{position:absolute;inset:auto 0 0 auto;display:block;width:2.25rem;height:2.25rem}`;
14
14
 
15
+ /**
16
+ * Avatar output defaults. These live in the component rather than in the shared
17
+ * `resizeImage` util (which is deliberately unopinionated): avatars are stored
18
+ * as a compact JPEG at a fixed quality. Callers can override them via `resize`.
19
+ */
20
+ const AVATAR_OUTPUT_TYPE = 'image/jpeg';
21
+ const AVATAR_JPEG_QUALITY = 0.85;
15
22
  const ProfilePicture = class {
16
23
  constructor(hostRef) {
17
24
  index.registerInstance(this, hostRef);
@@ -70,7 +77,7 @@ const ProfilePicture = class {
70
77
  return (index.h("limel-tooltip", { elementId: this.browseButtonId, label: this.helperText }));
71
78
  };
72
79
  this.handleNewFiles = async (event) => {
73
- var _a, _b;
80
+ var _a, _b, _c, _d;
74
81
  event.stopPropagation();
75
82
  if (this.disabled) {
76
83
  return;
@@ -89,10 +96,10 @@ const ProfilePicture = class {
89
96
  // Optional client-side resize
90
97
  if (this.resize && file.fileContent instanceof File) {
91
98
  try {
92
- const processed = await imageResize.resizeImage(file.fileContent, Object.assign(Object.assign({}, this.resize), { fit: (_b = this.resize.fit) !== null && _b !== void 0 ? _b : this.imageFit }));
99
+ const processed = await imageResize.resizeImage(file.fileContent, Object.assign(Object.assign({}, this.resize), { fit: (_b = this.resize.fit) !== null && _b !== void 0 ? _b : this.imageFit, type: (_c = this.resize.type) !== null && _c !== void 0 ? _c : AVATAR_OUTPUT_TYPE, quality: (_d = this.resize.quality) !== null && _d !== void 0 ? _d : AVATAR_JPEG_QUALITY }));
93
100
  out = Object.assign(Object.assign({}, file), { filename: processed.name, size: processed.size, contentType: processed.type, fileContent: processed });
94
101
  }
95
- catch (_c) {
102
+ catch (_e) {
96
103
  // Fall back to original file if resize fails
97
104
  out = file;
98
105
  }
@@ -15869,12 +15869,15 @@ function getImageNodeMarkdownSerializer(language, inlineImages) {
15869
15869
  * @param node
15870
15870
  */
15871
15871
  function applyImageStyles(img, node) {
15872
- img.style.height = node.attrs.height;
15873
- img.style.width = node.attrs.width;
15872
+ img.style.height = toCssLength(node.attrs.height);
15873
+ img.style.width = toCssLength(node.attrs.width);
15874
15874
  img.style.minHeight = node.attrs.minHeight;
15875
15875
  img.style.minWidth = node.attrs.minWidth;
15876
15876
  img.style.maxWidth = node.attrs.maxWidth;
15877
15877
  }
15878
+ function toCssLength(value) {
15879
+ return /^\d+$/.test(value) ? `${value}px` : value;
15880
+ }
15878
15881
  /**
15879
15882
  * Recursively checks if a ProseMirror node or any of its descendants is an
15880
15883
  * image node.
@@ -15920,11 +15923,6 @@ function escapeAttributeValue(value) {
15920
15923
  .replaceAll('<', '&lt;')
15921
15924
  .replaceAll('>', '&gt;');
15922
15925
  }
15923
- // Dimensions are emitted as CSS-unit strings (e.g. `width="300px"`), matching
15924
- // what `applyImageStyles` reads back when the tag is re-parsed inside the
15925
- // editor. The external renderer of this microformat (the `limebb-inline-image`
15926
- // building block) must therefore treat `width`/`height` as CSS lengths, not as
15927
- // bare HTML pixel counts.
15928
15926
  function getInlineImageHTML(attrs, tag) {
15929
15927
  var _a;
15930
15928
  const attributes = [
@@ -5,6 +5,13 @@ import translate from "../../global/translations";
5
5
  import { createRandomString } from "../../util/random-string";
6
6
  import { resizeImage } from "../../util/image-resize";
7
7
  import { ImageTemplate } from "../../util/image.template";
8
+ /**
9
+ * Avatar output defaults. These live in the component rather than in the shared
10
+ * `resizeImage` util (which is deliberately unopinionated): avatars are stored
11
+ * as a compact JPEG at a fixed quality. Callers can override them via `resize`.
12
+ */
13
+ const AVATAR_OUTPUT_TYPE = 'image/jpeg';
14
+ const AVATAR_JPEG_QUALITY = 0.85;
8
15
  /**
9
16
  * This component displays a profile picture, while allowing the user
10
17
  * to change it via a file input or drag-and-drop.
@@ -80,7 +87,7 @@ export class ProfilePicture {
80
87
  return (h("limel-tooltip", { elementId: this.browseButtonId, label: this.helperText }));
81
88
  };
82
89
  this.handleNewFiles = async (event) => {
83
- var _a, _b;
90
+ var _a, _b, _c, _d;
84
91
  event.stopPropagation();
85
92
  if (this.disabled) {
86
93
  return;
@@ -99,10 +106,10 @@ export class ProfilePicture {
99
106
  // Optional client-side resize
100
107
  if (this.resize && file.fileContent instanceof File) {
101
108
  try {
102
- const processed = await resizeImage(file.fileContent, Object.assign(Object.assign({}, this.resize), { fit: (_b = this.resize.fit) !== null && _b !== void 0 ? _b : this.imageFit }));
109
+ const processed = await resizeImage(file.fileContent, Object.assign(Object.assign({}, this.resize), { fit: (_b = this.resize.fit) !== null && _b !== void 0 ? _b : this.imageFit, type: (_c = this.resize.type) !== null && _c !== void 0 ? _c : AVATAR_OUTPUT_TYPE, quality: (_d = this.resize.quality) !== null && _d !== void 0 ? _d : AVATAR_JPEG_QUALITY }));
103
110
  out = Object.assign(Object.assign({}, file), { filename: processed.name, size: processed.size, contentType: processed.type, fileContent: processed });
104
111
  }
105
- catch (_c) {
112
+ catch (_e) {
106
113
  // Fall back to original file if resize fails
107
114
  out = file;
108
115
  }
@@ -556,7 +563,7 @@ export class ProfilePicture {
556
563
  "optional": true,
557
564
  "docs": {
558
565
  "tags": [],
559
- "text": "Optional client-side resize before emitting the file.\nIf provided, the selected image will be resized on the client device.\n:::note\nHEIC may not decode in all browsers; when decoding fails, the original\nfile will be emitted. See the examples for more info.\n:::"
566
+ "text": "Optional client-side resize before emitting the file.\nIf provided, the selected image will be resized on the client device.\nOmitted options fall back to avatar defaults: `image/jpeg`, `quality`\n`0.85`, and the component's `imageFit`.\n:::note\nHEIC may not decode in all browsers; when decoding fails, the original\nfile will be emitted. See the examples for more info.\n:::"
560
567
  },
561
568
  "getter": false,
562
569
  "setter": false
@@ -38,12 +38,15 @@ export function getImageNodeMarkdownSerializer(language, inlineImages) {
38
38
  * @param node
39
39
  */
40
40
  export function applyImageStyles(img, node) {
41
- img.style.height = node.attrs.height;
42
- img.style.width = node.attrs.width;
41
+ img.style.height = toCssLength(node.attrs.height);
42
+ img.style.width = toCssLength(node.attrs.width);
43
43
  img.style.minHeight = node.attrs.minHeight;
44
44
  img.style.minWidth = node.attrs.minWidth;
45
45
  img.style.maxWidth = node.attrs.maxWidth;
46
46
  }
47
+ function toCssLength(value) {
48
+ return /^\d+$/.test(value) ? `${value}px` : value;
49
+ }
47
50
  /**
48
51
  * Recursively checks if a ProseMirror node or any of its descendants is an
49
52
  * image node.
@@ -89,11 +92,6 @@ function escapeAttributeValue(value) {
89
92
  .replaceAll('<', '&lt;')
90
93
  .replaceAll('>', '&gt;');
91
94
  }
92
- // Dimensions are emitted as CSS-unit strings (e.g. `width="300px"`), matching
93
- // what `applyImageStyles` reads back when the tag is re-parsed inside the
94
- // editor. The external renderer of this microformat (the `limebb-inline-image`
95
- // building block) must therefore treat `width`/`height` as CSS lengths, not as
96
- // bare HTML pixel counts.
97
95
  function getInlineImageHTML(attrs, tag) {
98
96
  var _a;
99
97
  const attributes = [
@@ -12,15 +12,17 @@
12
12
  *
13
13
  * Why resize client-side?
14
14
  * - Faster perceived uploads and lower bandwidth usage
15
- * - Consistent avatar sizes and formats (e.g., JPEG 400x400)
15
+ * - Consistent output sizes and formats (e.g., a 400x400 thumbnail)
16
16
  * - No server-side transformation required for common cases
17
17
  *
18
18
  * Fit strategies
19
- * - `cover` (default): The image is scaled to cover the target rectangle, and
20
- * the excess parts are center-cropped. Good for avatars.
19
+ * - `cover`: The image is scaled to cover the target rectangle, and the excess
20
+ * parts are center-cropped. Good for avatars.
21
21
  * - `contain`: The image is scaled to fit entirely within the target rectangle
22
22
  * without cropping, letterboxing if needed. Good when you must preserve the
23
23
  * entire image.
24
+ * - omitted: scales the whole image to fit without cropping (no pixels are
25
+ * discarded).
24
26
  *
25
27
  * Decoding & EXIF orientation
26
28
  * EXIF orientation is a piece of metadata stored inside image files
@@ -46,9 +48,11 @@
46
48
  * library or WASM module; this utility intentionally avoids extra dependencies.
47
49
  *
48
50
  * Output type & quality
49
- * - Default output is `image/jpeg` with `quality=0.85`, which is typically
50
- * appropriate for avatars. You can switch to `image/png` to preserve
51
- * transparency.
51
+ * - When `type` is omitted the source format is kept if the canvas can encode
52
+ * it: a PNG stays a PNG (preserving transparency) and everything else is
53
+ * encoded as `image/jpeg`. Set `type` explicitly to force a format.
54
+ * - JPEG `quality` is optional; when omitted the browser's native encoding
55
+ * quality is used rather than an imposed value.
52
56
  * - The output filename extension is adjusted to match the chosen MIME type by
53
57
  * default (e.g., `.jpg` or `.png`). You can override naming via the `rename`
54
58
  * option.
@@ -73,9 +77,9 @@
73
77
  * const processed = await resizeImage(file, {
74
78
  * width: 400,
75
79
  * height: 400,
76
- * fit: 'cover', // default; center-crops
77
- * type: 'image/jpeg', // default
78
- * quality: 0.85, // default
80
+ * fit: 'cover', // center-crops; omit to fit without cropping
81
+ * type: 'image/jpeg', // omit to keep the source format
82
+ * quality: 0.85, // omit to use the browser's native quality
79
83
  * });
80
84
  * // Upload `processed` instead of the original file
81
85
  * ```
@@ -104,6 +108,17 @@
104
108
  * ```
105
109
  */
106
110
  // (Removed exported ResizeFit to avoid forcing a public symbol.)
111
+ /**
112
+ * The image formats the canvas can reliably encode, mapped to their filename
113
+ * extension. Single source of truth for what this util can output: both the
114
+ * output-type resolution and the output filename extension derive from it. The
115
+ * `ResizeOptions.type` union below mirrors these keys — keep the two in sync
116
+ * when adding a format.
117
+ */
118
+ const ENCODABLE_TYPES = {
119
+ 'image/jpeg': 'jpg',
120
+ 'image/png': 'png',
121
+ };
107
122
  /**
108
123
  * Resize an image file on the client using Canvas/OffscreenCanvas.
109
124
  * Returns a new File with the requested format and dimensions.
@@ -118,7 +133,17 @@
118
133
  * @param options - Configuration for the resize operation.
119
134
  */
120
135
  export async function resizeImage(file, options) {
121
- const { fit = 'cover', type = 'image/jpeg', quality = 0.85, rename = (name) => renameWithType(name, type), } = options;
136
+ var _a;
137
+ // This shared utility carries no opinionated defaults: an omitted option
138
+ // preserves the source file's own property rather than imposing a value.
139
+ // Consumers that want a specific default (e.g. limel-profile-picture) apply
140
+ // it themselves. See the `ResizeOptions` doc for the per-option behavior.
141
+ const { fit, quality } = options;
142
+ // When no output type is requested, keep the source file's format if the
143
+ // canvas can encode it (PNG stays PNG, preserving transparency); anything
144
+ // else falls back to JPEG.
145
+ const type = resolveOutputType(file.type, options.type);
146
+ const rename = (_a = options.rename) !== null && _a !== void 0 ? _a : ((name) => renameWithType(name, type));
122
147
  const source = await loadSource(file);
123
148
  const sourceWidth = source.width;
124
149
  const sourceHeight = source.height;
@@ -207,7 +232,7 @@ function get2dContext(canvas) {
207
232
  * Convert the canvas content to a Blob, supporting both canvas types.
208
233
  * @param canvas - The source canvas
209
234
  * @param type - Output MIME type
210
- * @param quality - JPEG quality (0..1)
235
+ * @param quality - JPEG quality (0..1); omit to use the browser default
211
236
  */
212
237
  function canvasToBlob(canvas, type, quality) {
213
238
  if ('convertToBlob' in canvas) {
@@ -293,7 +318,8 @@ async function loadImageElement(file) {
293
318
  * @param sh - Source height
294
319
  * @param tw - Target width
295
320
  * @param th - Target height
296
- * @param fit - Fit mode (cover/contain)
321
+ * @param fit - Fit mode; `cover` center-crops, anything else (including
322
+ * omitted) scales the whole image to fit without cropping
297
323
  */
298
324
  function computeRects(sw, sh, tw, th, fit) {
299
325
  const sRatio = sw / sh;
@@ -331,13 +357,30 @@ function computeRects(sw, sh, tw, th, fit) {
331
357
  const dy = (th - drawH) / 2;
332
358
  return { sx: 0, sy: 0, sw, sh, dx, dy, dw: drawW, dh: drawH };
333
359
  }
360
+ /**
361
+ * Resolve the output MIME type. An explicit request wins; otherwise the source
362
+ * file's format is kept when the canvas can encode it (see `ENCODABLE_TYPES`),
363
+ * so a PNG stays a PNG (preserving transparency) while every other input
364
+ * (WebP, HEIC, …) falls back to JPEG.
365
+ *
366
+ * @param sourceType - MIME type of the input file
367
+ * @param requested - Explicitly requested output type, if any
368
+ */
369
+ function resolveOutputType(sourceType, requested) {
370
+ if (requested) {
371
+ return requested;
372
+ }
373
+ // Keep the source format when the canvas can encode it; otherwise JPEG.
374
+ const canEncodeSource = Object.prototype.hasOwnProperty.call(ENCODABLE_TYPES, sourceType);
375
+ return canEncodeSource ? sourceType : 'image/jpeg';
376
+ }
334
377
  /**
335
378
  * Update filename extension to match the desired MIME type.
336
379
  * @param name - Original filename
337
380
  * @param type - Output MIME type
338
381
  */
339
382
  function renameWithType(name, type) {
340
- const ext = type === 'image/png' ? 'png' : 'jpg';
383
+ const ext = ENCODABLE_TYPES[type];
341
384
  const idx = name.lastIndexOf('.');
342
385
  const base = idx > 0 ? name.slice(0, idx) : name;
343
386
  return `${base}.${ext}`;
@@ -12,15 +12,17 @@
12
12
  *
13
13
  * Why resize client-side?
14
14
  * - Faster perceived uploads and lower bandwidth usage
15
- * - Consistent avatar sizes and formats (e.g., JPEG 400x400)
15
+ * - Consistent output sizes and formats (e.g., a 400x400 thumbnail)
16
16
  * - No server-side transformation required for common cases
17
17
  *
18
18
  * Fit strategies
19
- * - `cover` (default): The image is scaled to cover the target rectangle, and
20
- * the excess parts are center-cropped. Good for avatars.
19
+ * - `cover`: The image is scaled to cover the target rectangle, and the excess
20
+ * parts are center-cropped. Good for avatars.
21
21
  * - `contain`: The image is scaled to fit entirely within the target rectangle
22
22
  * without cropping, letterboxing if needed. Good when you must preserve the
23
23
  * entire image.
24
+ * - omitted: scales the whole image to fit without cropping (no pixels are
25
+ * discarded).
24
26
  *
25
27
  * Decoding & EXIF orientation
26
28
  * EXIF orientation is a piece of metadata stored inside image files
@@ -46,9 +48,11 @@
46
48
  * library or WASM module; this utility intentionally avoids extra dependencies.
47
49
  *
48
50
  * Output type & quality
49
- * - Default output is `image/jpeg` with `quality=0.85`, which is typically
50
- * appropriate for avatars. You can switch to `image/png` to preserve
51
- * transparency.
51
+ * - When `type` is omitted the source format is kept if the canvas can encode
52
+ * it: a PNG stays a PNG (preserving transparency) and everything else is
53
+ * encoded as `image/jpeg`. Set `type` explicitly to force a format.
54
+ * - JPEG `quality` is optional; when omitted the browser's native encoding
55
+ * quality is used rather than an imposed value.
52
56
  * - The output filename extension is adjusted to match the chosen MIME type by
53
57
  * default (e.g., `.jpg` or `.png`). You can override naming via the `rename`
54
58
  * option.
@@ -73,9 +77,9 @@
73
77
  * const processed = await resizeImage(file, {
74
78
  * width: 400,
75
79
  * height: 400,
76
- * fit: 'cover', // default; center-crops
77
- * type: 'image/jpeg', // default
78
- * quality: 0.85, // default
80
+ * fit: 'cover', // center-crops; omit to fit without cropping
81
+ * type: 'image/jpeg', // omit to keep the source format
82
+ * quality: 0.85, // omit to use the browser's native quality
79
83
  * });
80
84
  * // Upload `processed` instead of the original file
81
85
  * ```
@@ -104,6 +108,17 @@
104
108
  * ```
105
109
  */
106
110
  // (Removed exported ResizeFit to avoid forcing a public symbol.)
111
+ /**
112
+ * The image formats the canvas can reliably encode, mapped to their filename
113
+ * extension. Single source of truth for what this util can output: both the
114
+ * output-type resolution and the output filename extension derive from it. The
115
+ * `ResizeOptions.type` union below mirrors these keys — keep the two in sync
116
+ * when adding a format.
117
+ */
118
+ const ENCODABLE_TYPES = {
119
+ 'image/jpeg': 'jpg',
120
+ 'image/png': 'png',
121
+ };
107
122
  /**
108
123
  * Resize an image file on the client using Canvas/OffscreenCanvas.
109
124
  * Returns a new File with the requested format and dimensions.
@@ -118,7 +133,17 @@
118
133
  * @param options - Configuration for the resize operation.
119
134
  */
120
135
  async function resizeImage(file, options) {
121
- const { fit = 'cover', type = 'image/jpeg', quality = 0.85, rename = (name) => renameWithType(name, type), } = options;
136
+ var _a;
137
+ // This shared utility carries no opinionated defaults: an omitted option
138
+ // preserves the source file's own property rather than imposing a value.
139
+ // Consumers that want a specific default (e.g. limel-profile-picture) apply
140
+ // it themselves. See the `ResizeOptions` doc for the per-option behavior.
141
+ const { fit, quality } = options;
142
+ // When no output type is requested, keep the source file's format if the
143
+ // canvas can encode it (PNG stays PNG, preserving transparency); anything
144
+ // else falls back to JPEG.
145
+ const type = resolveOutputType(file.type, options.type);
146
+ const rename = (_a = options.rename) !== null && _a !== void 0 ? _a : ((name) => renameWithType(name, type));
122
147
  const source = await loadSource(file);
123
148
  const sourceWidth = source.width;
124
149
  const sourceHeight = source.height;
@@ -207,7 +232,7 @@ function get2dContext(canvas) {
207
232
  * Convert the canvas content to a Blob, supporting both canvas types.
208
233
  * @param canvas - The source canvas
209
234
  * @param type - Output MIME type
210
- * @param quality - JPEG quality (0..1)
235
+ * @param quality - JPEG quality (0..1); omit to use the browser default
211
236
  */
212
237
  function canvasToBlob(canvas, type, quality) {
213
238
  if ('convertToBlob' in canvas) {
@@ -293,7 +318,8 @@ async function loadImageElement(file) {
293
318
  * @param sh - Source height
294
319
  * @param tw - Target width
295
320
  * @param th - Target height
296
- * @param fit - Fit mode (cover/contain)
321
+ * @param fit - Fit mode; `cover` center-crops, anything else (including
322
+ * omitted) scales the whole image to fit without cropping
297
323
  */
298
324
  function computeRects(sw, sh, tw, th, fit) {
299
325
  const sRatio = sw / sh;
@@ -331,13 +357,30 @@ function computeRects(sw, sh, tw, th, fit) {
331
357
  const dy = (th - drawH) / 2;
332
358
  return { sx: 0, sy: 0, sw, sh, dx, dy, dw: drawW, dh: drawH };
333
359
  }
360
+ /**
361
+ * Resolve the output MIME type. An explicit request wins; otherwise the source
362
+ * file's format is kept when the canvas can encode it (see `ENCODABLE_TYPES`),
363
+ * so a PNG stays a PNG (preserving transparency) while every other input
364
+ * (WebP, HEIC, …) falls back to JPEG.
365
+ *
366
+ * @param sourceType - MIME type of the input file
367
+ * @param requested - Explicitly requested output type, if any
368
+ */
369
+ function resolveOutputType(sourceType, requested) {
370
+ if (requested) {
371
+ return requested;
372
+ }
373
+ // Keep the source format when the canvas can encode it; otherwise JPEG.
374
+ const canEncodeSource = Object.prototype.hasOwnProperty.call(ENCODABLE_TYPES, sourceType);
375
+ return canEncodeSource ? sourceType : 'image/jpeg';
376
+ }
334
377
  /**
335
378
  * Update filename extension to match the desired MIME type.
336
379
  * @param name - Original filename
337
380
  * @param type - Output MIME type
338
381
  */
339
382
  function renameWithType(name, type) {
340
- const ext = type === 'image/png' ? 'png' : 'jpg';
383
+ const ext = ENCODABLE_TYPES[type];
341
384
  const idx = name.lastIndexOf('.');
342
385
  const base = idx > 0 ? name.slice(0, idx) : name;
343
386
  return `${base}.${ext}`;
package/dist/esm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { g as globalConfig } from './config-Dnt5w_Bp.js';
2
2
  export { _ as _mapLayout } from './layout-C4zsCPOF.js';
3
3
  export { E as EditorMenuTypes, L as LevelMapping, M as MouseButtons, e as editorMenuTypesArray, i as isInlineImageTag } from './types-C_Yu8dOg.js';
4
- export { r as resizeImage } from './image-resize-65ma7B85.js';
4
+ export { r as resizeImage } from './image-resize-BTSG--2-.js';
5
5
  export { r as redrawComponents } from './dispatch-resize-event-z_E3sq3p.js';
6
6
 
7
7
  /**
@@ -2,7 +2,7 @@ import { r as registerInstance, c as createEvent, h, H as Host } from './index-B
2
2
  import { t as translate } from './translations-Cb9R_zmF.js';
3
3
  import { g as getFileBackgroundColor, a as getFileColor, b as getFileExtensionTitle, c as getFileIcon } from './file-metadata-BHzkIH4g.js';
4
4
  import { f as formatBytes } from './format-bytes-u_sd2HMb.js';
5
- import { r as resizeImage } from './image-resize-65ma7B85.js';
5
+ import { r as resizeImage } from './image-resize-BTSG--2-.js';
6
6
  import './icons-Bu_YRgCL.js';
7
7
  import './get-icon-props-CgNJbSP4.js';
8
8