jazz-tools 0.16.6 → 0.17.1
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 +47 -51
- package/CHANGELOG.md +24 -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/index.js.map +1 -1
- package/dist/media/chunk-JBLT443O.js +211 -0
- package/dist/media/chunk-JBLT443O.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/coValues/request.d.ts +1 -1
- package/dist/tools/coValues/request.d.ts.map +1 -1
- package/dist/tools/exports.d.ts +1 -1
- package/dist/tools/exports.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 +373 -0
- package/src/media/utils.ts +205 -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/coValues/request.ts +1 -1
- package/src/tools/exports.ts +1 -0
- 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,159 @@
|
|
1
|
+
import { FileStream, ImageDefinition } from "jazz-tools";
|
2
|
+
import { highestResAvailable } from "jazz-tools/media";
|
3
|
+
import { forwardRef, useEffect, useMemo, useState } from "react";
|
4
|
+
import { Image as RNImage, ImageProps as RNImageProps } from "react-native";
|
5
|
+
import { useCoState } from "../hooks.js";
|
6
|
+
|
7
|
+
export type ImageProps = Omit<RNImageProps, "width" | "height" | "source"> & {
|
8
|
+
/** The ID of the ImageDefinition to display */
|
9
|
+
imageId: string;
|
10
|
+
/**
|
11
|
+
* Width of the image. Can be a number or "original" to use the original image width.
|
12
|
+
* When set to "original", the component will calculate the appropriate height to maintain aspect ratio.
|
13
|
+
*
|
14
|
+
* @example
|
15
|
+
* ```tsx
|
16
|
+
* // Fixed width, auto-calculated height
|
17
|
+
* <Image imageId="123" width={600} />
|
18
|
+
*
|
19
|
+
* // Original width
|
20
|
+
* <Image imageId="123" width="original" />
|
21
|
+
* ```
|
22
|
+
*/
|
23
|
+
width?: number | "original";
|
24
|
+
/**
|
25
|
+
* Height of the image. Can be a number or "original" to use the original image height.
|
26
|
+
* When set to "original", the component will calculate the appropriate width to maintain aspect ratio.
|
27
|
+
*
|
28
|
+
* @example
|
29
|
+
* ```tsx
|
30
|
+
* // Fixed height, auto-calculated width
|
31
|
+
* <Image imageId="123" height={400} />
|
32
|
+
*
|
33
|
+
* // Original height
|
34
|
+
* <Image imageId="123" height="original" />
|
35
|
+
* ```
|
36
|
+
*/
|
37
|
+
height?: number | "original";
|
38
|
+
};
|
39
|
+
|
40
|
+
/**
|
41
|
+
* A React Native Image component that integrates with Jazz's ImageDefinition system.
|
42
|
+
*
|
43
|
+
* @example
|
44
|
+
* ```tsx
|
45
|
+
* import { Image } from "jazz-tools/react-native";
|
46
|
+
* import { StyleSheet } from "react-native";
|
47
|
+
*
|
48
|
+
* function ProfilePicture({ imageId }) {
|
49
|
+
* return (
|
50
|
+
* <Image
|
51
|
+
* imageId={imageId}
|
52
|
+
* style={styles.profilePic}
|
53
|
+
* width={100}
|
54
|
+
* height={100}
|
55
|
+
* resizeMode="cover"
|
56
|
+
* />
|
57
|
+
* );
|
58
|
+
* }
|
59
|
+
*
|
60
|
+
* const styles = StyleSheet.create({
|
61
|
+
* profilePic: {
|
62
|
+
* borderRadius: 50,
|
63
|
+
* }
|
64
|
+
* });
|
65
|
+
* ```
|
66
|
+
*/
|
67
|
+
export const Image = forwardRef<RNImage, ImageProps>(function Image(
|
68
|
+
{ imageId, width, height, ...props },
|
69
|
+
ref,
|
70
|
+
) {
|
71
|
+
const image = useCoState(ImageDefinition, imageId);
|
72
|
+
const [src, setSrc] = useState<string | undefined>(image?.placeholderDataURL);
|
73
|
+
|
74
|
+
const dimensions: { width: number | undefined; height: number | undefined } =
|
75
|
+
useMemo(() => {
|
76
|
+
const originalWidth = image?.originalSize?.[0];
|
77
|
+
const originalHeight = image?.originalSize?.[1];
|
78
|
+
|
79
|
+
// Both width and height are "original"
|
80
|
+
if (width === "original" && height === "original") {
|
81
|
+
return { width: originalWidth, height: originalHeight };
|
82
|
+
}
|
83
|
+
|
84
|
+
// Width is "original", height is a number
|
85
|
+
if (width === "original" && typeof height === "number") {
|
86
|
+
if (originalWidth && originalHeight) {
|
87
|
+
return {
|
88
|
+
width: Math.round((height * originalWidth) / originalHeight),
|
89
|
+
height,
|
90
|
+
};
|
91
|
+
}
|
92
|
+
return { width: undefined, height };
|
93
|
+
}
|
94
|
+
|
95
|
+
// Height is "original", width is a number
|
96
|
+
if (height === "original" && typeof width === "number") {
|
97
|
+
if (originalWidth && originalHeight) {
|
98
|
+
return {
|
99
|
+
width,
|
100
|
+
height: Math.round((width * originalHeight) / originalWidth),
|
101
|
+
};
|
102
|
+
}
|
103
|
+
return { width, height: undefined };
|
104
|
+
}
|
105
|
+
|
106
|
+
// In all other cases, use the property value:
|
107
|
+
return {
|
108
|
+
width: width === "original" ? originalWidth : width,
|
109
|
+
height: height === "original" ? originalHeight : height,
|
110
|
+
};
|
111
|
+
}, [image?.originalSize, width, height]);
|
112
|
+
|
113
|
+
useEffect(() => {
|
114
|
+
if (!image) return;
|
115
|
+
|
116
|
+
let lastBestImage: FileStream | string | undefined =
|
117
|
+
image.placeholderDataURL;
|
118
|
+
|
119
|
+
const unsub = image.subscribe({}, (update) => {
|
120
|
+
if (lastBestImage === undefined && update.placeholderDataURL) {
|
121
|
+
setSrc(update.placeholderDataURL);
|
122
|
+
lastBestImage = update.placeholderDataURL;
|
123
|
+
}
|
124
|
+
|
125
|
+
const bestImage = highestResAvailable(
|
126
|
+
update,
|
127
|
+
dimensions.width || dimensions.height || 9999,
|
128
|
+
dimensions.height || dimensions.width || 9999,
|
129
|
+
);
|
130
|
+
|
131
|
+
if (!bestImage) return;
|
132
|
+
|
133
|
+
if (lastBestImage === bestImage.image) return;
|
134
|
+
|
135
|
+
const url = bestImage.image.asBase64({ dataURL: true });
|
136
|
+
|
137
|
+
if (url) {
|
138
|
+
setSrc(url);
|
139
|
+
lastBestImage = bestImage.image;
|
140
|
+
}
|
141
|
+
});
|
142
|
+
|
143
|
+
return unsub;
|
144
|
+
}, [image]);
|
145
|
+
|
146
|
+
if (!image) {
|
147
|
+
return null;
|
148
|
+
}
|
149
|
+
|
150
|
+
return (
|
151
|
+
<RNImage
|
152
|
+
ref={ref}
|
153
|
+
source={{ uri: src }}
|
154
|
+
width={dimensions.width}
|
155
|
+
height={dimensions.height}
|
156
|
+
{...props}
|
157
|
+
/>
|
158
|
+
);
|
159
|
+
});
|
package/src/svelte/index.ts
CHANGED
@@ -0,0 +1,131 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import { ImageDefinition } from "jazz-tools";
|
3
|
+
import { highestResAvailable } from "jazz-tools/media";
|
4
|
+
import { onDestroy } from "svelte";
|
5
|
+
import type { HTMLImgAttributes } from "svelte/elements";
|
6
|
+
import { CoState } from "../jazz.class.svelte";
|
7
|
+
|
8
|
+
interface ImageProps extends Omit<HTMLImgAttributes, "width" | "height"> {
|
9
|
+
imageId: string;
|
10
|
+
width?: number | "original";
|
11
|
+
height?: number | "original";
|
12
|
+
}
|
13
|
+
|
14
|
+
const { imageId, width, height, ...rest }: ImageProps = $props();
|
15
|
+
|
16
|
+
const imageState = new CoState(ImageDefinition, () => imageId);
|
17
|
+
let lastBestImage: [string, string] | null = null;
|
18
|
+
|
19
|
+
/**
|
20
|
+
* For lazy loading, we use the browser's strategy for images with loading="lazy".
|
21
|
+
* We use an empty image, and when the browser triggers the load event, we load the best available image.
|
22
|
+
* On page loading, if the image url is already in browser's cache, the load event is triggered immediately.
|
23
|
+
* This is why we need to use a different blob url for every image.
|
24
|
+
*/
|
25
|
+
let waitingLazyLoading = $state(rest.loading === "lazy");
|
26
|
+
const lazyPlaceholder = $derived.by(() =>
|
27
|
+
waitingLazyLoading ? URL.createObjectURL(emptyPixelBlob) : undefined,
|
28
|
+
);
|
29
|
+
|
30
|
+
const dimensions = $derived.by<{
|
31
|
+
width: number | undefined;
|
32
|
+
height: number | undefined;
|
33
|
+
}>(() => {
|
34
|
+
const originalWidth = imageState.current?.originalSize?.[0];
|
35
|
+
const originalHeight = imageState.current?.originalSize?.[1];
|
36
|
+
|
37
|
+
// Both width and height are "original"
|
38
|
+
if (width === "original" && height === "original") {
|
39
|
+
return { width: originalWidth, height: originalHeight };
|
40
|
+
}
|
41
|
+
|
42
|
+
// Width is "original", height is a number
|
43
|
+
if (width === "original" && typeof height === "number") {
|
44
|
+
if (originalWidth && originalHeight) {
|
45
|
+
return {
|
46
|
+
width: Math.round((height * originalWidth) / originalHeight),
|
47
|
+
height,
|
48
|
+
};
|
49
|
+
}
|
50
|
+
return { width: undefined, height };
|
51
|
+
}
|
52
|
+
|
53
|
+
// Height is "original", width is a number
|
54
|
+
if (height === "original" && typeof width === "number") {
|
55
|
+
if (originalWidth && originalHeight) {
|
56
|
+
return {
|
57
|
+
width,
|
58
|
+
height: Math.round((width * originalHeight) / originalWidth),
|
59
|
+
};
|
60
|
+
}
|
61
|
+
return { width, height: undefined };
|
62
|
+
}
|
63
|
+
|
64
|
+
// In all other cases, use the property value:
|
65
|
+
return {
|
66
|
+
width: width === "original" ? originalWidth : width,
|
67
|
+
height: height === "original" ? originalHeight : height,
|
68
|
+
};
|
69
|
+
});
|
70
|
+
|
71
|
+
const src = $derived.by(() => {
|
72
|
+
if (waitingLazyLoading) {
|
73
|
+
return lazyPlaceholder;
|
74
|
+
}
|
75
|
+
|
76
|
+
const image = imageState.current;
|
77
|
+
if (!image) return undefined;
|
78
|
+
|
79
|
+
const bestImage = highestResAvailable(
|
80
|
+
image,
|
81
|
+
dimensions.width || dimensions.height || 9999,
|
82
|
+
dimensions.height || dimensions.width || 9999,
|
83
|
+
);
|
84
|
+
|
85
|
+
if (!bestImage) return image.placeholderDataURL;
|
86
|
+
if (lastBestImage?.[0] === bestImage.image.id) return lastBestImage?.[1];
|
87
|
+
|
88
|
+
const blob = bestImage.image.toBlob();
|
89
|
+
|
90
|
+
if (blob) {
|
91
|
+
const url = URL.createObjectURL(blob);
|
92
|
+
revokeObjectURL(lastBestImage?.[1]);
|
93
|
+
lastBestImage = [bestImage.image.id, url];
|
94
|
+
return url;
|
95
|
+
}
|
96
|
+
|
97
|
+
return image.placeholderDataURL;
|
98
|
+
});
|
99
|
+
|
100
|
+
// Cleanup object URL on component destroy
|
101
|
+
onDestroy(() => {
|
102
|
+
revokeObjectURL(lastBestImage?.[1]);
|
103
|
+
});
|
104
|
+
|
105
|
+
function revokeObjectURL(url: string | undefined) {
|
106
|
+
if (url && url.startsWith("blob:")) {
|
107
|
+
URL.revokeObjectURL(url);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
const emptyPixelBlob = new Blob(
|
112
|
+
[
|
113
|
+
Uint8Array.from(
|
114
|
+
atob(
|
115
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
116
|
+
),
|
117
|
+
(c) => c.charCodeAt(0),
|
118
|
+
),
|
119
|
+
],
|
120
|
+
{ type: "image/png" },
|
121
|
+
);
|
122
|
+
</script>
|
123
|
+
|
124
|
+
<img
|
125
|
+
{src}
|
126
|
+
width={dimensions.width}
|
127
|
+
height={dimensions.height}
|
128
|
+
alt={rest.alt}
|
129
|
+
onload={() => {waitingLazyLoading = false}}
|
130
|
+
{...rest}
|
131
|
+
/>
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default as Image } from "./image.svelte";
|