@pylonsync/react 0.22.22 → 0.22.23

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/dist/Image.d.ts CHANGED
@@ -33,7 +33,8 @@ export interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElemen
33
33
  quality?: PylonImageQuality;
34
34
  /**
35
35
  * Override the candidate widths used in `srcset`. By default we
36
- * emit 1x and 2x of `width`, capped at 3840px.
36
+ * emit every allowed width from 256px up to 2x of `width` (rounded
37
+ * down to an allowed width), plus the 1x width.
37
38
  */
38
39
  widths?: number[];
39
40
  /**
@@ -54,4 +55,19 @@ export interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElemen
54
55
  */
55
56
  unoptimized?: boolean;
56
57
  }
58
+ /**
59
+ * srcset widths for an image displayed at `width` CSS px.
60
+ *
61
+ * Every allowed width from MIN_CANDIDATE up to the largest one that does
62
+ * not exceed 2x, plus the 1x width. The browser then picks by `sizes` and
63
+ * device pixel ratio, so a small slot gets a small file.
64
+ *
65
+ * The 2x candidate rounds DOWN. Rounding up turned a 1200px image's 2x
66
+ * (2400) into a 3840px request, larger than most sources and several
67
+ * times the bytes.
68
+ */
69
+ export declare function imageCandidateWidths(width: number, widths?: number[]): {
70
+ widths: number[];
71
+ oneX: number;
72
+ };
57
73
  export declare function Image({ src, width, height, alt, quality, widths, sizes, priority, unoptimized, className, style, ...rest }: ImageProps): React.JSX.Element;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.22.22",
6
+ "version": "0.22.23",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -14,9 +14,9 @@
14
14
  "prepack": "bun run build"
15
15
  },
16
16
  "dependencies": {
17
- "@pylonsync/realtime": "0.22.22",
18
- "@pylonsync/sdk": "0.22.22",
19
- "@pylonsync/sync": "0.22.22"
17
+ "@pylonsync/realtime": "0.22.23",
18
+ "@pylonsync/sdk": "0.22.23",
19
+ "@pylonsync/sync": "0.22.23"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "react": ">=19.0.0"
@@ -0,0 +1,31 @@
1
+ import { expect, test } from "bun:test";
2
+ import { imageCandidateWidths } from "./Image";
3
+
4
+ test("2x rounds down to an allowed width instead of jumping to 3840", () => {
5
+ const { widths, oneX } = imageCandidateWidths(1200);
6
+ expect(oneX).toBe(1200);
7
+ expect(Math.max(...widths)).toBe(2048);
8
+ expect(widths).not.toContain(3840);
9
+ });
10
+
11
+ test("offers smaller widths so `sizes` can pick a small file", () => {
12
+ const { widths } = imageCandidateWidths(452);
13
+ expect(widths).toEqual([256, 384, 640, 750, 828]);
14
+ });
15
+
16
+ test("the 1x width is always offered, even when it is above 2x of a tiny image", () => {
17
+ const { widths, oneX } = imageCandidateWidths(20);
18
+ expect(oneX).toBe(32);
19
+ expect(widths).toContain(32);
20
+ });
21
+
22
+ test("very large images cap at 3840", () => {
23
+ const { widths, oneX } = imageCandidateWidths(5000);
24
+ expect(oneX).toBe(3840);
25
+ expect(Math.max(...widths)).toBe(3840);
26
+ });
27
+
28
+ test("explicit widths are used as given, deduplicated and sorted", () => {
29
+ expect(imageCandidateWidths(500, [1080, 640, 640]).widths).toEqual([640, 1080]);
30
+ expect(imageCandidateWidths(500, [1080, 640]).oneX).toBe(640);
31
+ });
package/src/Image.tsx CHANGED
@@ -65,7 +65,8 @@ export interface ImageProps
65
65
  quality?: PylonImageQuality;
66
66
  /**
67
67
  * Override the candidate widths used in `srcset`. By default we
68
- * emit 1x and 2x of `width`, capped at 3840px.
68
+ * emit every allowed width from 256px up to 2x of `width` (rounded
69
+ * down to an allowed width), plus the 1x width.
69
70
  */
70
71
  widths?: number[];
71
72
  /**
@@ -115,6 +116,36 @@ function optimizedUrl(src: string, width: number, quality: number): string {
115
116
  return `/_pylon/image?src=${encodeURIComponent(src)}&w=${w}&q=${q}`;
116
117
  }
117
118
 
119
+ /** Smallest ladder width offered in a default srcset. */
120
+ const MIN_CANDIDATE = 256;
121
+
122
+ /**
123
+ * srcset widths for an image displayed at `width` CSS px.
124
+ *
125
+ * Every allowed width from MIN_CANDIDATE up to the largest one that does
126
+ * not exceed 2x, plus the 1x width. The browser then picks by `sizes` and
127
+ * device pixel ratio, so a small slot gets a small file.
128
+ *
129
+ * The 2x candidate rounds DOWN. Rounding up turned a 1200px image's 2x
130
+ * (2400) into a 3840px request, larger than most sources and several
131
+ * times the bytes.
132
+ */
133
+ export function imageCandidateWidths(
134
+ width: number,
135
+ widths?: number[],
136
+ ): { widths: number[]; oneX: number } {
137
+ if (widths && widths.length > 0) {
138
+ const list = Array.from(new Set(widths.map((w) => Math.round(w)))).sort((a, b) => a - b);
139
+ return { widths: list, oneX: list.find((w) => w >= width) ?? list[list.length - 1] };
140
+ }
141
+ const oneX = DEFAULT_WIDTHS.find((w) => w >= width) ?? MAX_WIDTH;
142
+ const twoXCap = Math.min(width * 2, MAX_WIDTH);
143
+ const list = DEFAULT_WIDTHS.filter(
144
+ (w) => w >= Math.min(MIN_CANDIDATE, oneX) && w <= Math.max(oneX, twoXCap),
145
+ );
146
+ return { widths: list, oneX };
147
+ }
148
+
118
149
  export function Image({
119
150
  src,
120
151
  width,
@@ -148,25 +179,10 @@ export function Image({
148
179
  );
149
180
  }
150
181
 
151
- // Candidate widths: explicit list, else default ladder filtered
152
- // to "smallest that satisfies 1x" through "smallest that
153
- // satisfies 2x", picked from DEFAULT_WIDTHS. Picking from the
154
- // ladder (rather than [width, width*2]) keeps URLs in the
155
- // server's allowed-widths set so we never get back a 400.
156
- const candidates = (() => {
157
- if (widths && widths.length > 0) {
158
- return Array.from(new Set(widths.map((w) => Math.round(w)))).sort(
159
- (a, b) => a - b,
160
- );
161
- }
162
- const oneX = DEFAULT_WIDTHS.find((w) => w >= width) ?? MAX_WIDTH;
163
- const targetTwoX = Math.min(width * 2, MAX_WIDTH);
164
- const twoX = DEFAULT_WIDTHS.find((w) => w >= targetTwoX) ?? MAX_WIDTH;
165
- return Array.from(new Set([oneX, twoX])).sort((a, b) => a - b);
166
- })();
167
-
168
- const baseSrc = optimizedUrl(src, candidates[candidates.length - 1], quality);
169
- const srcSet = candidates
182
+ const candidates = imageCandidateWidths(width, widths);
183
+ // The 1x candidate is the fallback for browsers that ignore srcset.
184
+ const baseSrc = optimizedUrl(src, candidates.oneX, quality);
185
+ const srcSet = candidates.widths
170
186
  .map((w) => `${optimizedUrl(src, w, quality)} ${w}w`)
171
187
  .join(", ");
172
188