@massimo-cassandro/minimo 1.14.2 → 1.14.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.14.2",
3
+ "version": "1.14.4",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -2,6 +2,12 @@ import { classnames } from './classnames.js';
2
2
 
3
3
  /** @typedef {import('./dom-builder/dom-builder.js').DomBuilderItem} DomBuilderItem */
4
4
 
5
+ /**
6
+ * @typedef {Object} PictureSizeEntry
7
+ * @property {number} [from] - Breakpoint (px) the size applies from (`width >=` media condition); omitted in the (single) mobile-first default entry.
8
+ * @property {number | [number, number]} size - Rendered width (px, height computed with `ratio`) or explicit `[width, height]` pair (px).
9
+ */
10
+
5
11
  // TODO aggiungere compatibilità con immagini Unsplash basate su imgix
6
12
 
7
13
  /**
@@ -9,9 +15,9 @@ import { classnames } from './classnames.js';
9
15
  *
10
16
  * Every `source`/`img` `srcset` is built with width descriptors (`w`): for each entry
11
17
  * of `sizes`, a candidate is generated for every integer density from 1 to `dpr`.
12
- * The `sizes` attribute pairs each entry of `sizes` with the corresponding `breakpoints`
13
- * entry as a `(max-width: …)` media condition; the last entry (without breakpoint) is
14
- * the default, so `breakpoints` is expected to have one element less than `sizes`.
18
+ * The `sizes` attribute is built mobile-first: the entry without `from` is the
19
+ * default, and every other entry applies from its `from` breakpoint up, as a
20
+ * `(width >= …)` media condition (range syntax).
15
21
  *
16
22
  * @example
17
23
  * // fixed-width thumb (250px, 16:9 ratio, 1x and 2x candidates)
@@ -19,33 +25,30 @@ import { classnames } from './classnames.js';
19
25
  * // sizes="250px", srcset="… 250w, … 500w"
20
26
  *
21
27
  * @example
22
- * // responsive image: 250px up to 576px, 500px up to 992px, 800px beyond.
23
- * // NB: `breakpoints` has one entry less than `sizes`: the missing one
24
- * // corresponds to the *last* `sizes` entry, used as default beyond the last breakpoint
28
+ * // responsive image: 250px by default, 500px from 576px, 800px from 992px.
29
+ * // NB: the entry without `from` is the mobile-first default (exactly one is
30
+ * // expected); a bare number or [width, height] pair is a shorthand for it
25
31
  * buildPictureTag({
26
32
  * baseSrc,
27
- * sizes: [250, 500, 800],
28
- * breakpoints: [576, 992],
33
+ * sizes: [250, { from: 576, size: 500 }, { from: 992, size: 800 }],
29
34
  * });
30
- * // sizes="(max-width: 576px) 250px, (max-width: 992px) 500px, 800px"
35
+ * // sizes="(width >= 992px) 800px, (width >= 576px) 500px, 250px"
31
36
  *
32
37
  * @example
33
- * // custom ratio for numeric entries and candidates up to 3x
38
+ * // custom ratio for numeric sizes and candidates up to 3x
34
39
  * buildPictureTag({
35
40
  * baseSrc,
36
- * sizes: [250, 500],
37
- * breakpoints: [768],
41
+ * sizes: [250, { from: 768, size: 500 }],
38
42
  * ratio: 4/3,
39
43
  * dpr: 3,
40
44
  * });
41
45
  *
42
46
  * @example
43
47
  * // different aspect ratio per breakpoint: explicit [width, height] pairs
44
- * // (can be mixed with numeric entries, which use `ratio`)
48
+ * // (can be mixed with numeric sizes, which use `ratio`)
45
49
  * buildPictureTag({
46
50
  * baseSrc,
47
- * sizes: [[250, 250], [800, 450]], // square on mobile, 16:9 on desktop
48
- * breakpoints: [576],
51
+ * sizes: [[250, 250], { from: 576, size: [800, 450] }], // square on mobile, 16:9 on desktop
49
52
  * });
50
53
  *
51
54
  * @example
@@ -54,15 +57,15 @@ import { classnames } from './classnames.js';
54
57
  *
55
58
  * @param {Object} args
56
59
  * @param {string} args.baseSrc - Image base URL (viewer endpoint); its query params are preserved.
57
- * @param {(number | [number, number])[]} [args.sizes=[250]] - Rendered image sizes, in ascending width order, one per breakpoint plus the default (entries beyond `breakpoints.length + 1` are ignored in the `sizes` attribute). Each entry is a width (px, height computed with `ratio`) or an explicit `[width, height]` pair (px), allowing a different aspect ratio per breakpoint.
58
- * @param {number} [args.ratio=16/9] - Default aspect ratio (width / height, as in the CSS `aspect-ratio` property) used to compute the height of numeric `sizes` entries.
60
+ * @param {(number | [number, number] | PictureSizeEntry)[]} [args.sizes=[250]] - Rendered image sizes, in any order. Each entry is a `{from, size}` object (see {@link PictureSizeEntry}); a bare number or `[width, height]` pair is a shorthand for the mobile-first default entry (the one without `from`), expected exactly once.
61
+ * @param {number} [args.ratio=16/9] - Default aspect ratio (width / height, as in the CSS `aspect-ratio` property) used to compute the height of numeric sizes.
59
62
  * @param {number} [args.dpr=2] - Maximum pixel density (integer): `srcset` candidates are generated for every density from 1 to `dpr`.
60
- * @param {number[]} [args.breakpoints=[]] - `max-width` breakpoints (px) paired with `sizes` (one element less than `sizes`).
61
63
  * @param {[string, string|number][]} [args.img_params=[['q','60'],['fd',1]]] - Query params (`[name, value]` pairs) added to every generated URL.
62
64
  * @param {string[]} [args.formats=['avif','webp','pjpg']] - Image formats: the last one is used for the `img` fallback element, the others for the `source` elements.
63
65
  * @param {boolean} [args.devMode=false] - When true, `avif` is excluded from `formats` (not supported by the local dev environment).
64
66
  * @param {boolean} [args.lazy=true] - When true, the `img` element gets `loading="lazy"`.
65
67
  * @param {boolean} [args.condition=true] - When false, the function returns `null` without building anything.
68
+ * @param {boolean} [args.legacyMediaSyntax=false] - When true, the `sizes` media conditions use `(min-width: …)` instead of the range syntax `(width >= …)`, for older browsers (pre Chrome/Edge 104, Firefox 102, Safari 16.4).
66
69
  * @param {string|null} [args.pictureExtraClass=null] - Extra class(es) for the `picture` element.
67
70
  * @param {Object<string, *>} [args.pictureExtraAttrs={}] - Extra attributes for the `picture` element.
68
71
  * @param {string|null} [args.imgExtraClass=null] - Extra class(es) for the `img` element.
@@ -75,12 +78,12 @@ export function buildPictureTag({
75
78
  sizes = [250],
76
79
  ratio = 16/9,
77
80
  dpr = 2,
78
- breakpoints = [],
79
81
  img_params = [ ['q', '60'] ],
80
82
  formats = ['avif', 'webp', 'pjpg'],
81
83
  devMode = false,
82
84
  lazy = true,
83
85
  condition = true,
86
+ legacyMediaSyntax = false,
84
87
  pictureExtraClass = null,
85
88
  pictureExtraAttrs = {},
86
89
  imgExtraClass = null,
@@ -100,46 +103,56 @@ export function buildPictureTag({
100
103
  formats = formats.filter(fmt => fmt !== 'avif');
101
104
  }
102
105
 
103
- if(sizes.length > breakpoints.length + 1) {
104
- // eslint-disable-next-line no-console
105
- console.warn('[buildPictureTag] `sizes` should have one entry more than `breakpoints`: extra entries are ignored in the `sizes` attribute');
106
- }
107
-
108
106
  // densità generate: da 1 a `dpr`
109
107
  const dprList = Array.from({length: dpr}, (_, i) => i + 1);
110
108
 
109
+ // entry di `sizes` normalizzate in oggetti {from, width, height}
110
+ // (per le size numeriche l'altezza è calcolata con `ratio`),
111
+ // ordinate per breakpoint, con il default (from = null) per primo
112
+ const sizeEntries = sizes.map(entry => {
113
+ if(typeof entry === 'object' && !Array.isArray(entry)) {
114
+ const [width, height] = Array.isArray(entry.size)? entry.size : [entry.size, Math.floor(entry.size / ratio)];
115
+ return { from: entry.from ?? null, width, height };
116
+ }
117
+ const [width, height] = Array.isArray(entry)? entry : [entry, Math.floor(entry / ratio)];
118
+ return { from: null, width, height };
119
+ }).sort((a, b) => (a.from ?? 0) - (b.from ?? 0));
120
+
121
+ // entry default (mobile-first, senza `from`): deve essere esattamente una
122
+ const defaultEntry = sizeEntries.find(e => e.from == null) ?? sizeEntries[0]
123
+ ,brkEntries = sizeEntries.filter(e => e.from != null && e !== defaultEntry);
124
+
125
+ if(sizeEntries.filter(e => e.from == null).length !== 1) {
126
+ // eslint-disable-next-line no-console
127
+ console.warn('[buildPictureTag] `sizes` must contain exactly one entry without `from` (the mobile-first default)');
128
+ }
129
+
111
130
  const base_url = new URL(baseSrc)
112
131
  ,searchParams = new URLSearchParams(base_url.search)
113
132
 
114
- // sizes normalizzato in coppie [width, height]:
115
- // per le entry numeriche l'altezza è calcolata con `ratio`
116
- ,sizePairs = sizes.map(size => Array.isArray(size)
117
- ? size
118
- : /** @type {[number, number]} */ ([size, Math.floor(size / ratio)])
119
- )
120
-
121
- // dimensioni dei candidati srcset (una per ogni densità in `dpr`),
133
+ // dimensioni dei candidati srcset (una per ogni densità da 1 a `dpr`),
122
134
  // senza duplicati (per larghezza), in ordine crescente
123
- ,srcsetSizes = sizePairs
124
- .flatMap(([w, h]) => dprList.map(d => /** @type {[number, number]} */ ([w * d, h * d])))
135
+ ,srcsetSizes = sizeEntries
136
+ .flatMap(({width, height}) => dprList.map(d => /** @type {[number, number]} */ ([width * d, height * d])))
125
137
  .filter((pair, idx, arr) => arr.findIndex(p => p[0] === pair[0]) === idx)
126
138
  .sort((a, b) => a[0] - b[0])
127
139
 
128
- // attributo `sizes`: ogni size è abbinata al breakpoint corrispondente (max-width),
129
- // quella successiva all'ultimo breakpoint è il valore di default
130
- // (le eventuali entry oltre `breakpoints.length + 1` vengono ignorate)
131
- ,sizesAttr = sizePairs
132
- .slice(0, breakpoints.length + 1)
133
- .map(([w], i) => i < breakpoints.length? `(max-width: ${breakpoints[i]}px) ${w}px` : `${w}px`)
140
+ // attributo `sizes` (mobile-first): le entry con breakpoint in ordine decrescente
141
+ // (vince la prima condizione che matcha), il default per ultimo
142
+ ,sizesAttr = brkEntries
143
+ .map(({from, width}) => legacyMediaSyntax
144
+ ? `(min-width: ${from}px) ${width}px`
145
+ : `(width >= ${from}px) ${width}px`
146
+ )
147
+ .reverse()
148
+ .concat(`${defaultEntry.width}px`)
134
149
  .join(', ')
135
150
 
136
- // dimensioni intrinseche dell'elemento img: la size più grande,
137
- // indipendentemente dall'ordine delle entry
138
- ,[imgWidth, imgHeight] = sizePairs.reduce((max, pair) => pair[0] > max[0]? pair : max)
151
+ // dimensioni intrinseche dell'elemento img: la size più grande
152
+ ,{width: imgWidth, height: imgHeight} = sizeEntries.reduce((max, entry) => entry.width > max.width? entry : max)
139
153
 
140
- // larghezza 1x della size mediana (mediana inferiore se le size sono pari),
141
- // indipendentemente dall'ordine delle entry
142
- ,medianWidth = sizePairs.map(([w]) => w).sort((a, b) => a - b)[Math.floor((sizePairs.length - 1) / 2)]
154
+ // larghezza 1x della size mediana (mediana inferiore se le size sono pari)
155
+ ,medianWidth = sizeEntries.map(({width}) => width).sort((a, b) => a - b)[Math.floor((sizeEntries.length - 1) / 2)]
143
156
 
144
157
  // indice del candidato srcset usato come src di default dell'img
145
158
  ,defaultSrcIdx = srcsetSizes.findIndex(([w]) => w === medianWidth)