jazz-tools 0.16.6 → 0.17.0
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/.svelte-kit/__package__/index.d.ts +1 -0
- package/.svelte-kit/__package__/index.d.ts.map +1 -1
- package/.svelte-kit/__package__/index.js +1 -0
- package/.svelte-kit/__package__/media/image.svelte +131 -0
- package/.svelte-kit/__package__/media/image.svelte.d.ts +10 -0
- package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -0
- package/.svelte-kit/__package__/media/index.d.ts +2 -0
- package/.svelte-kit/__package__/media/index.d.ts.map +1 -0
- package/.svelte-kit/__package__/media/index.js +1 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts +2 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.js +430 -0
- package/.svelte-kit/__package__/tests/testUtils.d.ts +11 -0
- package/.svelte-kit/__package__/tests/testUtils.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/testUtils.js +17 -0
- package/.svelte-kit/__package__/tests/types.d.ts +3 -0
- package/.turbo/turbo-build.log +42 -46
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-R2VNCMG6.js → chunk-2SH44VLX.js} +33 -38
- package/dist/chunk-2SH44VLX.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/media/chunk-BBSS3NEY.js +211 -0
- package/dist/media/chunk-BBSS3NEY.js.map +1 -0
- package/dist/media/create-image.d.ts +48 -0
- package/dist/media/create-image.d.ts.map +1 -0
- package/dist/media/create-image.test.d.ts +2 -0
- package/dist/media/create-image.test.d.ts.map +1 -0
- package/dist/media/index.browser.d.ts +15 -0
- package/dist/media/index.browser.d.ts.map +1 -0
- package/dist/media/index.browser.js +113 -0
- package/dist/media/index.browser.js.map +1 -0
- package/dist/media/index.d.ts +53 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +13 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/index.native.d.ts +17 -0
- package/dist/media/index.native.d.ts.map +1 -0
- package/dist/media/index.native.js +126 -0
- package/dist/media/index.native.js.map +1 -0
- package/dist/media/utils.d.ts +17 -0
- package/dist/media/utils.d.ts.map +1 -0
- package/dist/media/utils.test.d.ts +2 -0
- package/dist/media/utils.test.d.ts.map +1 -0
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +176 -59
- package/dist/react/index.js.map +1 -1
- package/dist/react/media/image.d.ts +62 -0
- package/dist/react/media/image.d.ts.map +1 -0
- package/dist/react/tests/media/image.test.d.ts +2 -0
- package/dist/react/tests/media/image.test.d.ts.map +1 -0
- package/dist/react-core/tests/useDemoAuth.test.d.ts +2 -0
- package/dist/react-core/tests/useDemoAuth.test.d.ts.map +1 -0
- package/dist/react-native-core/index.d.ts +1 -1
- package/dist/react-native-core/index.d.ts.map +1 -1
- package/dist/react-native-core/index.js +84 -66
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/media/image.d.ts +93 -0
- package/dist/react-native-core/media/image.d.ts.map +1 -0
- package/dist/react-native-core/testing.d.ts +2 -0
- package/dist/react-native-core/testing.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -0
- package/dist/svelte/media/image.svelte +131 -0
- package/dist/svelte/media/image.svelte.d.ts +10 -0
- package/dist/svelte/media/image.svelte.d.ts.map +1 -0
- package/dist/svelte/media/index.d.ts +2 -0
- package/dist/svelte/media/index.d.ts.map +1 -0
- package/dist/svelte/media/index.js +1 -0
- package/dist/svelte/tests/media/image.svelte.test.d.ts +2 -0
- package/dist/svelte/tests/media/image.svelte.test.d.ts.map +1 -0
- package/dist/svelte/tests/media/image.svelte.test.js +430 -0
- package/dist/svelte/tests/testUtils.d.ts +11 -0
- package/dist/svelte/tests/testUtils.d.ts.map +1 -0
- package/dist/svelte/tests/testUtils.js +17 -0
- package/dist/svelte/tests/types.d.ts +3 -0
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/coFeed.d.ts +15 -0
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/extensions/imageDef.d.ts +3 -9
- package/dist/tools/coValues/extensions/imageDef.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +1 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/media/create-image.test.ts +195 -0
- package/src/media/create-image.ts +180 -0
- package/src/media/index.browser.ts +150 -0
- package/src/media/index.native.ts +153 -0
- package/src/media/index.ts +61 -0
- package/src/media/utils.test.ts +327 -0
- package/src/media/utils.ts +202 -0
- package/src/react/index.ts +1 -2
- package/src/react/media/image.tsx +210 -0
- package/src/react/tests/media/image.test.tsx +588 -0
- package/src/react-native-core/index.ts +1 -1
- package/src/react-native-core/media/image.tsx +159 -0
- package/src/svelte/index.ts +1 -0
- package/src/svelte/media/image.svelte +131 -0
- package/src/svelte/media/index.ts +1 -0
- package/src/svelte/tests/media/image.svelte.test.ts +583 -0
- package/src/svelte/tests/testUtils.ts +33 -0
- package/src/svelte/tests/types.d.ts +3 -0
- package/src/tools/coValues/coFeed.ts +37 -5
- package/src/tools/coValues/extensions/imageDef.ts +3 -49
- package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +6 -0
- package/src/tools/tests/coMap.record.test.ts +3 -2
- package/src/tools/tests/coOptional.test.ts +3 -1
- package/tsconfig.json +1 -0
- package/tsup.config.ts +4 -9
- package/vitest.config.ts +14 -1
- package/dist/browser-media-images/index.d.ts +0 -9
- package/dist/browser-media-images/index.d.ts.map +0 -1
- package/dist/browser-media-images/index.js +0 -72
- package/dist/browser-media-images/index.js.map +0 -1
- package/dist/chunk-R2VNCMG6.js.map +0 -1
- package/dist/react/media.d.ts +0 -24
- package/dist/react/media.d.ts.map +0 -1
- package/dist/react-native-core/media.d.ts +0 -24
- package/dist/react-native-core/media.d.ts.map +0 -1
- package/dist/react-native-media-images/index.d.ts +0 -7
- package/dist/react-native-media-images/index.d.ts.map +0 -1
- package/dist/react-native-media-images/index.js +0 -177
- package/dist/react-native-media-images/index.js.map +0 -1
- package/dist/tools/tests/imageDef.test.d.ts +0 -2
- package/dist/tools/tests/imageDef.test.d.ts.map +0 -1
- package/src/browser-media-images/index.ts +0 -131
- package/src/react/media.tsx +0 -74
- package/src/react/scratch.tsx +0 -50
- package/src/react-native-core/media.tsx +0 -79
- package/src/react-native-media-images/index.ts +0 -238
- package/src/tools/tests/imageDef.test.ts +0 -278
@@ -0,0 +1,210 @@
|
|
1
|
+
import { ImageDefinition } from "jazz-tools";
|
2
|
+
import {
|
3
|
+
type JSX,
|
4
|
+
forwardRef,
|
5
|
+
useCallback,
|
6
|
+
useEffect,
|
7
|
+
useMemo,
|
8
|
+
useRef,
|
9
|
+
useState,
|
10
|
+
} from "react";
|
11
|
+
import { highestResAvailable } from "../../media/index.js";
|
12
|
+
import { useCoState } from "../hooks.js";
|
13
|
+
|
14
|
+
export type ImageProps = Omit<
|
15
|
+
JSX.IntrinsicElements["img"],
|
16
|
+
"src" | "srcSet" | "width" | "height"
|
17
|
+
> & {
|
18
|
+
/** The ID of the ImageDefinition to display */
|
19
|
+
imageId: string;
|
20
|
+
/**
|
21
|
+
* The desired width of the image. Can be a number in pixels or "original" to use the image's original width.
|
22
|
+
* When set to a number, the component will select the best available resolution and maintain aspect ratio.
|
23
|
+
*
|
24
|
+
* @example
|
25
|
+
* ```tsx
|
26
|
+
* // Use original width
|
27
|
+
* <Image imageId="123" width="original" />
|
28
|
+
*
|
29
|
+
* // Set width to 600px, height will be calculated to maintain aspect ratio
|
30
|
+
* <Image imageId="123" width={600} />
|
31
|
+
*
|
32
|
+
* // Set both width and height to maintain aspect ratio
|
33
|
+
* <Image imageId="123" width={600} height={400} />
|
34
|
+
* ```
|
35
|
+
*/
|
36
|
+
width?: number | "original";
|
37
|
+
/**
|
38
|
+
* The desired height of the image. Can be a number in pixels or "original" to use the image's original height.
|
39
|
+
* When set to a number, the component will select the best available resolution and maintain aspect ratio.
|
40
|
+
*
|
41
|
+
* @example
|
42
|
+
* ```tsx
|
43
|
+
* // Use original height
|
44
|
+
* <Image imageId="123" height="original" />
|
45
|
+
*
|
46
|
+
* // Set height to 400px, width will be calculated to maintain aspect ratio
|
47
|
+
* <Image imageId="123" height={400} />
|
48
|
+
*
|
49
|
+
* // Set both width and height to maintain aspect ratio
|
50
|
+
* <Image imageId="123" width={600} height={400} />
|
51
|
+
* ```
|
52
|
+
*/
|
53
|
+
height?: number | "original";
|
54
|
+
};
|
55
|
+
|
56
|
+
/**
|
57
|
+
* A React component for displaying images stored as ImageDefinition CoValues.
|
58
|
+
*
|
59
|
+
* @example
|
60
|
+
* ```tsx
|
61
|
+
* import { Image } from "jazz-tools/react";
|
62
|
+
*
|
63
|
+
* // Force specific dimensions (may crop or stretch)
|
64
|
+
* function Avatar({ imageId }: { imageId: string }) {
|
65
|
+
* return (
|
66
|
+
* <Image
|
67
|
+
* imageId={imageId}
|
68
|
+
* width={100}
|
69
|
+
* height={100}
|
70
|
+
* alt="Avatar"
|
71
|
+
* style={{ borderRadius: "50%", objectFit: "cover" }}
|
72
|
+
* />
|
73
|
+
* );
|
74
|
+
* }
|
75
|
+
* ```
|
76
|
+
*/
|
77
|
+
export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
|
78
|
+
{ imageId, width, height, ...props },
|
79
|
+
ref,
|
80
|
+
) {
|
81
|
+
const image = useCoState(ImageDefinition, imageId);
|
82
|
+
const lastBestImage = useRef<[string, string] | null>(null);
|
83
|
+
|
84
|
+
/**
|
85
|
+
* For lazy loading, we use the browser's strategy for images with loading="lazy".
|
86
|
+
* We use an empty image, and when the browser triggers the load event, we load the best available image.
|
87
|
+
* On page loading, if the image url is already in browser's cache, the load event is triggered immediately.
|
88
|
+
* This is why we need to use a different blob url for every image.
|
89
|
+
*/
|
90
|
+
const [waitingLazyLoading, setWaitingLazyLoading] = useState(
|
91
|
+
props.loading === "lazy",
|
92
|
+
);
|
93
|
+
const lazyPlaceholder = useMemo(
|
94
|
+
() =>
|
95
|
+
waitingLazyLoading ? URL.createObjectURL(emptyPixelBlob) : undefined,
|
96
|
+
[waitingLazyLoading],
|
97
|
+
);
|
98
|
+
|
99
|
+
const dimensions: { width: number | undefined; height: number | undefined } =
|
100
|
+
useMemo(() => {
|
101
|
+
const originalWidth = image?.originalSize?.[0];
|
102
|
+
const originalHeight = image?.originalSize?.[1];
|
103
|
+
|
104
|
+
// Both width and height are "original"
|
105
|
+
if (width === "original" && height === "original") {
|
106
|
+
return { width: originalWidth, height: originalHeight };
|
107
|
+
}
|
108
|
+
|
109
|
+
// Width is "original", height is a number
|
110
|
+
if (width === "original" && typeof height === "number") {
|
111
|
+
if (originalWidth && originalHeight) {
|
112
|
+
return {
|
113
|
+
width: Math.round((height * originalWidth) / originalHeight),
|
114
|
+
height,
|
115
|
+
};
|
116
|
+
}
|
117
|
+
return { width: undefined, height };
|
118
|
+
}
|
119
|
+
|
120
|
+
// Height is "original", width is a number
|
121
|
+
if (height === "original" && typeof width === "number") {
|
122
|
+
if (originalWidth && originalHeight) {
|
123
|
+
return {
|
124
|
+
width,
|
125
|
+
height: Math.round((width * originalHeight) / originalWidth),
|
126
|
+
};
|
127
|
+
}
|
128
|
+
return { width, height: undefined };
|
129
|
+
}
|
130
|
+
|
131
|
+
// In all other cases, use the property value:
|
132
|
+
return {
|
133
|
+
width: width === "original" ? originalWidth : width,
|
134
|
+
height: height === "original" ? originalHeight : height,
|
135
|
+
};
|
136
|
+
}, [image?.originalSize, width, height]);
|
137
|
+
|
138
|
+
const src = useMemo(() => {
|
139
|
+
if (waitingLazyLoading) {
|
140
|
+
return lazyPlaceholder;
|
141
|
+
}
|
142
|
+
|
143
|
+
if (!image) return undefined;
|
144
|
+
|
145
|
+
const bestImage = highestResAvailable(
|
146
|
+
image,
|
147
|
+
dimensions.width || dimensions.height || 9999,
|
148
|
+
dimensions.height || dimensions.width || 9999,
|
149
|
+
);
|
150
|
+
|
151
|
+
if (!bestImage) return image.placeholderDataURL;
|
152
|
+
if (lastBestImage.current?.[0] === bestImage.image.id)
|
153
|
+
return lastBestImage.current?.[1];
|
154
|
+
|
155
|
+
const blob = bestImage.image.toBlob();
|
156
|
+
|
157
|
+
if (blob) {
|
158
|
+
const url = URL.createObjectURL(blob);
|
159
|
+
revokeObjectURL(lastBestImage.current?.[1]);
|
160
|
+
lastBestImage.current = [bestImage.image.id, url];
|
161
|
+
return url;
|
162
|
+
}
|
163
|
+
|
164
|
+
return image.placeholderDataURL;
|
165
|
+
}, [image, dimensions.width, dimensions.height, waitingLazyLoading]);
|
166
|
+
|
167
|
+
const onThresholdReached = useCallback(() => {
|
168
|
+
setWaitingLazyLoading(false);
|
169
|
+
}, []);
|
170
|
+
|
171
|
+
// Revoke object URL when component unmounts
|
172
|
+
useEffect(
|
173
|
+
() => () => {
|
174
|
+
// In development mode we don't revokeObjectURL on unmount because
|
175
|
+
// it triggers twice under StrictMode.
|
176
|
+
if (process.env.NODE_ENV === "development") return;
|
177
|
+
revokeObjectURL(lastBestImage.current?.[1]);
|
178
|
+
},
|
179
|
+
[],
|
180
|
+
);
|
181
|
+
|
182
|
+
return (
|
183
|
+
<img
|
184
|
+
ref={ref}
|
185
|
+
src={src}
|
186
|
+
width={dimensions.width}
|
187
|
+
height={dimensions.height}
|
188
|
+
onLoad={waitingLazyLoading ? onThresholdReached : undefined}
|
189
|
+
{...props}
|
190
|
+
/>
|
191
|
+
);
|
192
|
+
});
|
193
|
+
|
194
|
+
function revokeObjectURL(url: string | undefined) {
|
195
|
+
if (url && url.startsWith("blob:")) {
|
196
|
+
URL.revokeObjectURL(url);
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
const emptyPixelBlob = new Blob(
|
201
|
+
[
|
202
|
+
Uint8Array.from(
|
203
|
+
atob(
|
204
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
205
|
+
),
|
206
|
+
(c) => c.charCodeAt(0),
|
207
|
+
),
|
208
|
+
],
|
209
|
+
{ type: "image/png" },
|
210
|
+
);
|