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.
Files changed (132) hide show
  1. package/.svelte-kit/__package__/index.d.ts +1 -0
  2. package/.svelte-kit/__package__/index.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/index.js +1 -0
  4. package/.svelte-kit/__package__/media/image.svelte +131 -0
  5. package/.svelte-kit/__package__/media/image.svelte.d.ts +10 -0
  6. package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -0
  7. package/.svelte-kit/__package__/media/index.d.ts +2 -0
  8. package/.svelte-kit/__package__/media/index.d.ts.map +1 -0
  9. package/.svelte-kit/__package__/media/index.js +1 -0
  10. package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts +2 -0
  11. package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts.map +1 -0
  12. package/.svelte-kit/__package__/tests/media/image.svelte.test.js +430 -0
  13. package/.svelte-kit/__package__/tests/testUtils.d.ts +11 -0
  14. package/.svelte-kit/__package__/tests/testUtils.d.ts.map +1 -0
  15. package/.svelte-kit/__package__/tests/testUtils.js +17 -0
  16. package/.svelte-kit/__package__/tests/types.d.ts +3 -0
  17. package/.turbo/turbo-build.log +42 -46
  18. package/CHANGELOG.md +12 -0
  19. package/dist/{chunk-R2VNCMG6.js → chunk-2SH44VLX.js} +33 -38
  20. package/dist/chunk-2SH44VLX.js.map +1 -0
  21. package/dist/index.js +1 -1
  22. package/dist/media/chunk-BBSS3NEY.js +211 -0
  23. package/dist/media/chunk-BBSS3NEY.js.map +1 -0
  24. package/dist/media/create-image.d.ts +48 -0
  25. package/dist/media/create-image.d.ts.map +1 -0
  26. package/dist/media/create-image.test.d.ts +2 -0
  27. package/dist/media/create-image.test.d.ts.map +1 -0
  28. package/dist/media/index.browser.d.ts +15 -0
  29. package/dist/media/index.browser.d.ts.map +1 -0
  30. package/dist/media/index.browser.js +113 -0
  31. package/dist/media/index.browser.js.map +1 -0
  32. package/dist/media/index.d.ts +53 -0
  33. package/dist/media/index.d.ts.map +1 -0
  34. package/dist/media/index.js +13 -0
  35. package/dist/media/index.js.map +1 -0
  36. package/dist/media/index.native.d.ts +17 -0
  37. package/dist/media/index.native.d.ts.map +1 -0
  38. package/dist/media/index.native.js +126 -0
  39. package/dist/media/index.native.js.map +1 -0
  40. package/dist/media/utils.d.ts +17 -0
  41. package/dist/media/utils.d.ts.map +1 -0
  42. package/dist/media/utils.test.d.ts +2 -0
  43. package/dist/media/utils.test.d.ts.map +1 -0
  44. package/dist/react/index.d.ts +1 -2
  45. package/dist/react/index.d.ts.map +1 -1
  46. package/dist/react/index.js +176 -59
  47. package/dist/react/index.js.map +1 -1
  48. package/dist/react/media/image.d.ts +62 -0
  49. package/dist/react/media/image.d.ts.map +1 -0
  50. package/dist/react/tests/media/image.test.d.ts +2 -0
  51. package/dist/react/tests/media/image.test.d.ts.map +1 -0
  52. package/dist/react-core/tests/useDemoAuth.test.d.ts +2 -0
  53. package/dist/react-core/tests/useDemoAuth.test.d.ts.map +1 -0
  54. package/dist/react-native-core/index.d.ts +1 -1
  55. package/dist/react-native-core/index.d.ts.map +1 -1
  56. package/dist/react-native-core/index.js +84 -66
  57. package/dist/react-native-core/index.js.map +1 -1
  58. package/dist/react-native-core/media/image.d.ts +93 -0
  59. package/dist/react-native-core/media/image.d.ts.map +1 -0
  60. package/dist/react-native-core/testing.d.ts +2 -0
  61. package/dist/react-native-core/testing.d.ts.map +1 -0
  62. package/dist/svelte/index.d.ts +1 -0
  63. package/dist/svelte/index.d.ts.map +1 -1
  64. package/dist/svelte/index.js +1 -0
  65. package/dist/svelte/media/image.svelte +131 -0
  66. package/dist/svelte/media/image.svelte.d.ts +10 -0
  67. package/dist/svelte/media/image.svelte.d.ts.map +1 -0
  68. package/dist/svelte/media/index.d.ts +2 -0
  69. package/dist/svelte/media/index.d.ts.map +1 -0
  70. package/dist/svelte/media/index.js +1 -0
  71. package/dist/svelte/tests/media/image.svelte.test.d.ts +2 -0
  72. package/dist/svelte/tests/media/image.svelte.test.d.ts.map +1 -0
  73. package/dist/svelte/tests/media/image.svelte.test.js +430 -0
  74. package/dist/svelte/tests/testUtils.d.ts +11 -0
  75. package/dist/svelte/tests/testUtils.d.ts.map +1 -0
  76. package/dist/svelte/tests/testUtils.js +17 -0
  77. package/dist/svelte/tests/types.d.ts +3 -0
  78. package/dist/testing.js +1 -1
  79. package/dist/tools/coValues/coFeed.d.ts +15 -0
  80. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  81. package/dist/tools/coValues/extensions/imageDef.d.ts +3 -9
  82. package/dist/tools/coValues/extensions/imageDef.d.ts.map +1 -1
  83. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +1 -0
  84. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
  85. package/package.json +12 -12
  86. package/src/media/create-image.test.ts +195 -0
  87. package/src/media/create-image.ts +180 -0
  88. package/src/media/index.browser.ts +150 -0
  89. package/src/media/index.native.ts +153 -0
  90. package/src/media/index.ts +61 -0
  91. package/src/media/utils.test.ts +327 -0
  92. package/src/media/utils.ts +202 -0
  93. package/src/react/index.ts +1 -2
  94. package/src/react/media/image.tsx +210 -0
  95. package/src/react/tests/media/image.test.tsx +588 -0
  96. package/src/react-native-core/index.ts +1 -1
  97. package/src/react-native-core/media/image.tsx +159 -0
  98. package/src/svelte/index.ts +1 -0
  99. package/src/svelte/media/image.svelte +131 -0
  100. package/src/svelte/media/index.ts +1 -0
  101. package/src/svelte/tests/media/image.svelte.test.ts +583 -0
  102. package/src/svelte/tests/testUtils.ts +33 -0
  103. package/src/svelte/tests/types.d.ts +3 -0
  104. package/src/tools/coValues/coFeed.ts +37 -5
  105. package/src/tools/coValues/extensions/imageDef.ts +3 -49
  106. package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +6 -0
  107. package/src/tools/tests/coMap.record.test.ts +3 -2
  108. package/src/tools/tests/coOptional.test.ts +3 -1
  109. package/tsconfig.json +1 -0
  110. package/tsup.config.ts +4 -9
  111. package/vitest.config.ts +14 -1
  112. package/dist/browser-media-images/index.d.ts +0 -9
  113. package/dist/browser-media-images/index.d.ts.map +0 -1
  114. package/dist/browser-media-images/index.js +0 -72
  115. package/dist/browser-media-images/index.js.map +0 -1
  116. package/dist/chunk-R2VNCMG6.js.map +0 -1
  117. package/dist/react/media.d.ts +0 -24
  118. package/dist/react/media.d.ts.map +0 -1
  119. package/dist/react-native-core/media.d.ts +0 -24
  120. package/dist/react-native-core/media.d.ts.map +0 -1
  121. package/dist/react-native-media-images/index.d.ts +0 -7
  122. package/dist/react-native-media-images/index.d.ts.map +0 -1
  123. package/dist/react-native-media-images/index.js +0 -177
  124. package/dist/react-native-media-images/index.js.map +0 -1
  125. package/dist/tools/tests/imageDef.test.d.ts +0 -2
  126. package/dist/tools/tests/imageDef.test.d.ts.map +0 -1
  127. package/src/browser-media-images/index.ts +0 -131
  128. package/src/react/media.tsx +0 -74
  129. package/src/react/scratch.tsx +0 -50
  130. package/src/react-native-core/media.tsx +0 -79
  131. package/src/react-native-media-images/index.ts +0 -238
  132. 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
+ );