@slopmachine/react 0.1.10 → 0.1.13
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/Agents.md +34 -0
- package/README.md +1 -1
- package/dist/index.d.mts +62 -3
- package/dist/index.d.ts +62 -3
- package/dist/index.js +287 -6
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +290 -6
- package/dist/index.mjs.map +1 -0
- package/package.json +3 -2
- package/src/components/SlopImage.tsx +155 -121
- package/src/components/SlopVideo.tsx +312 -0
- package/src/index.ts +8 -1
- package/tsconfig.json +1 -0
- package/tsup.config.ts +5 -4
|
@@ -2,20 +2,50 @@ import React, { useMemo, useState } from "react";
|
|
|
2
2
|
import { clsx, type ClassValue } from "clsx";
|
|
3
3
|
import { twMerge } from "tailwind-merge";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildImageUrl,
|
|
7
|
+
type SlopImageOptions,
|
|
8
|
+
type ImageAspectRatio,
|
|
9
|
+
} from "@slopmachine/core";
|
|
6
10
|
|
|
7
11
|
// Utility for Tailwind classes
|
|
8
12
|
function cn(...inputs: ClassValue[]) {
|
|
9
13
|
return twMerge(clsx(inputs));
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
interface SlopImageProps
|
|
16
|
+
export interface SlopImageProps
|
|
13
17
|
extends
|
|
14
18
|
Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">,
|
|
15
|
-
SlopImageOptions {
|
|
19
|
+
Omit<SlopImageOptions, "aspectRatio"> {
|
|
20
|
+
/**
|
|
21
|
+
* The aspect ratio of the generated image. Defaults to "1:1".
|
|
22
|
+
*/
|
|
23
|
+
aspectRatio?: ImageAspectRatio;
|
|
24
|
+
/**
|
|
25
|
+
* Custom React node to display while the image is loading.
|
|
26
|
+
* If not provided, a default spinner and shimmer effect will be shown.
|
|
27
|
+
*/
|
|
16
28
|
loader?: React.ReactNode;
|
|
17
29
|
}
|
|
18
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Renders an image generated by Slop Machine.
|
|
33
|
+
*
|
|
34
|
+
* This component automatically constructs the correct URL for the Slop Machine API,
|
|
35
|
+
* displays a loading skeleton or custom loader while fetching, and handles errors.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <SlopImage
|
|
40
|
+
* bucketId="my-bucket"
|
|
41
|
+
* resultId="some-result"
|
|
42
|
+
* aspectRatio="16:9"
|
|
43
|
+
* className="rounded-lg"
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @version 0.1.13
|
|
48
|
+
*/
|
|
19
49
|
export const SlopImage: React.FC<SlopImageProps> = ({
|
|
20
50
|
bucketId,
|
|
21
51
|
className,
|
|
@@ -107,45 +137,23 @@ export const SlopImage: React.FC<SlopImageProps> = ({
|
|
|
107
137
|
}
|
|
108
138
|
}
|
|
109
139
|
`}</style>
|
|
110
|
-
<div
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
125
|
-
)}
|
|
126
|
-
style={{
|
|
127
|
-
position: "absolute",
|
|
128
|
-
top: 0,
|
|
129
|
-
left: 0,
|
|
130
|
-
right: 0,
|
|
131
|
-
bottom: 0,
|
|
132
|
-
zIndex: 10,
|
|
133
|
-
display: "flex",
|
|
134
|
-
alignItems: "center",
|
|
135
|
-
justifyContent: "center",
|
|
136
|
-
opacity: isLoading ? 1 : 0,
|
|
137
|
-
pointerEvents: isLoading ? "auto" : "none",
|
|
138
|
-
transition: "opacity 300ms ease-in-out",
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
{loader}
|
|
142
|
-
</div>
|
|
143
|
-
) : (
|
|
144
|
-
<>
|
|
145
|
-
{/* Loading overlay */}
|
|
140
|
+
<div className={className} style={props.style}>
|
|
141
|
+
<div
|
|
142
|
+
className="slop-wrapper relative overflow-hidden w-full h-full"
|
|
143
|
+
style={{
|
|
144
|
+
aspectRatio: String(aspectRatio).replace(":", "/"),
|
|
145
|
+
backgroundColor: "var(--muted, #f3f4f6)",
|
|
146
|
+
position: "relative",
|
|
147
|
+
overflow: "hidden",
|
|
148
|
+
width: "100%",
|
|
149
|
+
height: "100%",
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{/* Loading UI */}
|
|
153
|
+
{loader ? (
|
|
146
154
|
<div
|
|
147
155
|
className={cn(
|
|
148
|
-
"absolute inset-0 z-10 flex items-center justify-center
|
|
156
|
+
"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
|
|
149
157
|
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
150
158
|
)}
|
|
151
159
|
style={{
|
|
@@ -158,104 +166,130 @@ export const SlopImage: React.FC<SlopImageProps> = ({
|
|
|
158
166
|
display: "flex",
|
|
159
167
|
alignItems: "center",
|
|
160
168
|
justifyContent: "center",
|
|
161
|
-
backgroundColor: "var(--muted, #f3f4f6)",
|
|
162
169
|
opacity: isLoading ? 1 : 0,
|
|
163
170
|
pointerEvents: isLoading ? "auto" : "none",
|
|
164
171
|
transition: "opacity 300ms ease-in-out",
|
|
165
172
|
}}
|
|
166
173
|
>
|
|
174
|
+
{loader}
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<>
|
|
178
|
+
{/* Loading overlay */}
|
|
167
179
|
<div
|
|
168
|
-
className=
|
|
180
|
+
className={cn(
|
|
181
|
+
"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
|
|
182
|
+
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
183
|
+
)}
|
|
169
184
|
style={{
|
|
185
|
+
position: "absolute",
|
|
186
|
+
top: 0,
|
|
187
|
+
left: 0,
|
|
188
|
+
right: 0,
|
|
189
|
+
bottom: 0,
|
|
190
|
+
zIndex: 10,
|
|
170
191
|
display: "flex",
|
|
171
|
-
flexDirection: "column",
|
|
172
192
|
alignItems: "center",
|
|
173
|
-
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
backgroundColor: "var(--muted, #f3f4f6)",
|
|
195
|
+
opacity: isLoading ? 1 : 0,
|
|
196
|
+
pointerEvents: isLoading ? "auto" : "none",
|
|
197
|
+
transition: "opacity 300ms ease-in-out",
|
|
174
198
|
}}
|
|
175
199
|
>
|
|
176
|
-
<
|
|
177
|
-
className="
|
|
178
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
179
|
-
fill="none"
|
|
180
|
-
viewBox="0 0 24 24"
|
|
200
|
+
<div
|
|
201
|
+
className="flex flex-col items-center gap-2"
|
|
181
202
|
style={{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
display: "flex",
|
|
204
|
+
flexDirection: "column",
|
|
205
|
+
alignItems: "center",
|
|
206
|
+
gap: "0.5rem",
|
|
186
207
|
}}
|
|
187
208
|
>
|
|
188
|
-
<
|
|
189
|
-
className="
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
<svg
|
|
210
|
+
className="spinner size-6 text-muted-foreground"
|
|
211
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
212
|
+
fill="none"
|
|
213
|
+
viewBox="0 0 24 24"
|
|
214
|
+
style={{
|
|
215
|
+
width: "24px",
|
|
216
|
+
height: "24px",
|
|
217
|
+
color: "var(--muted-foreground, #6b7280)",
|
|
218
|
+
animation: "slop-spin 1s linear infinite",
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<circle
|
|
222
|
+
className="opacity-25"
|
|
223
|
+
cx="12"
|
|
224
|
+
cy="12"
|
|
225
|
+
r="10"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
strokeWidth="4"
|
|
228
|
+
style={{ opacity: 0.25 }}
|
|
229
|
+
/>
|
|
230
|
+
<path
|
|
231
|
+
className="opacity-75"
|
|
232
|
+
fill="currentColor"
|
|
233
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
234
|
+
style={{ opacity: 0.75 }}
|
|
235
|
+
/>
|
|
236
|
+
</svg>
|
|
237
|
+
<span
|
|
238
|
+
className="text-xs text-muted-foreground"
|
|
239
|
+
style={{
|
|
240
|
+
fontSize: "0.75rem",
|
|
241
|
+
color: "var(--muted-foreground, #6b7280)",
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
Loading...
|
|
245
|
+
</span>
|
|
246
|
+
</div>
|
|
213
247
|
</div>
|
|
214
|
-
</div>
|
|
215
248
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)}
|
|
238
|
-
|
|
239
|
-
{/* Image */}
|
|
240
|
-
<img
|
|
241
|
-
key={src}
|
|
242
|
-
src={src}
|
|
243
|
-
alt={alt}
|
|
244
|
-
onLoad={() => setIsLoading(false)}
|
|
245
|
-
onError={() => setIsLoading(false)}
|
|
246
|
-
className={cn(
|
|
247
|
-
"h-full w-full object-cover transition-opacity duration-500",
|
|
248
|
-
isLoading ? "opacity-0" : "opacity-100",
|
|
249
|
+
{/* Shimmer effect underneath */}
|
|
250
|
+
<div
|
|
251
|
+
className={cn(
|
|
252
|
+
"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
|
|
253
|
+
isLoading ? "opacity-100" : "opacity-0",
|
|
254
|
+
)}
|
|
255
|
+
style={{
|
|
256
|
+
position: "absolute",
|
|
257
|
+
top: 0,
|
|
258
|
+
left: 0,
|
|
259
|
+
right: 0,
|
|
260
|
+
bottom: 0,
|
|
261
|
+
background:
|
|
262
|
+
"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
|
|
263
|
+
backgroundSize: "200% 100%",
|
|
264
|
+
animation: "slop-shimmer 2s ease-in-out infinite",
|
|
265
|
+
opacity: isLoading ? 1 : 0,
|
|
266
|
+
transition: "opacity 300ms",
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
</>
|
|
249
270
|
)}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
271
|
+
|
|
272
|
+
{/* Image */}
|
|
273
|
+
<img
|
|
274
|
+
key={src}
|
|
275
|
+
src={src}
|
|
276
|
+
alt={alt}
|
|
277
|
+
onLoad={() => setIsLoading(false)}
|
|
278
|
+
onError={() => setIsLoading(false)}
|
|
279
|
+
className={cn(
|
|
280
|
+
"h-full w-full object-cover transition-opacity duration-500",
|
|
281
|
+
isLoading ? "opacity-0" : "opacity-100",
|
|
282
|
+
)}
|
|
283
|
+
style={{
|
|
284
|
+
height: "100%",
|
|
285
|
+
width: "100%",
|
|
286
|
+
objectFit: "cover",
|
|
287
|
+
transition: "opacity 500ms",
|
|
288
|
+
opacity: isLoading ? 0 : 1,
|
|
289
|
+
}}
|
|
290
|
+
{...props}
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
259
293
|
</div>
|
|
260
294
|
</>
|
|
261
295
|
);
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import React, { useMemo, useState, useRef, useEffect } from "react";
|
|
2
|
+
import { clsx, type ClassValue } from "clsx";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildVideoUrl,
|
|
7
|
+
type SlopVideoOptions,
|
|
8
|
+
type VideoAspectRatio,
|
|
9
|
+
} from "@slopmachine/core";
|
|
10
|
+
|
|
11
|
+
// Utility for Tailwind classes
|
|
12
|
+
function cn(...inputs: ClassValue[]) {
|
|
13
|
+
return twMerge(clsx(inputs));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SlopVideoProps
|
|
17
|
+
extends
|
|
18
|
+
Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "src">,
|
|
19
|
+
Omit<SlopVideoOptions, "aspectRatio"> {
|
|
20
|
+
/**
|
|
21
|
+
* The aspect ratio of the generated video. Defaults to "16:9".
|
|
22
|
+
*/
|
|
23
|
+
aspectRatio?: VideoAspectRatio;
|
|
24
|
+
/**
|
|
25
|
+
* Custom React node to display while the video is loading.
|
|
26
|
+
* If not provided, a default spinner and shimmer effect will be shown.
|
|
27
|
+
*/
|
|
28
|
+
loader?: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Renders a video generated by Slop Machine.
|
|
33
|
+
*
|
|
34
|
+
* This component automatically constructs the correct URL for the Slop Machine API,
|
|
35
|
+
* displays a loading skeleton or custom loader while fetching, and handles errors.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <SlopVideo
|
|
40
|
+
* bucketId="my-bucket"
|
|
41
|
+
* resultId="some-result"
|
|
42
|
+
* aspectRatio="16:9"
|
|
43
|
+
* className="rounded-lg"
|
|
44
|
+
* autoPlay
|
|
45
|
+
* loop
|
|
46
|
+
* muted
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export const SlopVideo: React.FC<SlopVideoProps> = ({
|
|
51
|
+
bucketId,
|
|
52
|
+
className,
|
|
53
|
+
aspectRatio = "16:9",
|
|
54
|
+
version,
|
|
55
|
+
resultId,
|
|
56
|
+
model,
|
|
57
|
+
variables = {},
|
|
58
|
+
duration,
|
|
59
|
+
baseUrl,
|
|
60
|
+
loader,
|
|
61
|
+
autoPlay = true,
|
|
62
|
+
loop = true,
|
|
63
|
+
muted = true,
|
|
64
|
+
playsInline = true,
|
|
65
|
+
...props
|
|
66
|
+
}) => {
|
|
67
|
+
// Construct API URL
|
|
68
|
+
const rawSrc = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
buildVideoUrl({
|
|
71
|
+
bucketId,
|
|
72
|
+
aspectRatio,
|
|
73
|
+
version,
|
|
74
|
+
resultId,
|
|
75
|
+
variables,
|
|
76
|
+
duration,
|
|
77
|
+
baseUrl,
|
|
78
|
+
model,
|
|
79
|
+
}),
|
|
80
|
+
[
|
|
81
|
+
bucketId,
|
|
82
|
+
aspectRatio,
|
|
83
|
+
version,
|
|
84
|
+
resultId,
|
|
85
|
+
variables,
|
|
86
|
+
duration,
|
|
87
|
+
baseUrl,
|
|
88
|
+
model,
|
|
89
|
+
],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const [src, setSrc] = useState(rawSrc);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
setSrc(rawSrc);
|
|
97
|
+
}, 100);
|
|
98
|
+
return () => clearTimeout(timer);
|
|
99
|
+
}, [rawSrc]);
|
|
100
|
+
|
|
101
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
102
|
+
const [currentSrc, setCurrentSrc] = useState(src);
|
|
103
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
104
|
+
|
|
105
|
+
if (src !== currentSrc) {
|
|
106
|
+
setCurrentSrc(src);
|
|
107
|
+
setIsLoading(true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!src) return;
|
|
112
|
+
|
|
113
|
+
fetch(src, { method: "HEAD" })
|
|
114
|
+
.then(async (res) => {
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
try {
|
|
117
|
+
const errRes = await fetch(src);
|
|
118
|
+
const errJson = await errRes.json();
|
|
119
|
+
console.error("SlopVideo error:", res.status, errJson.error);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Failed to fetch or parse error details
|
|
122
|
+
}
|
|
123
|
+
setIsLoading(false);
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.catch((err) => {
|
|
127
|
+
console.error(`Error fetching video from ${src}:`, err);
|
|
128
|
+
setIsLoading(false);
|
|
129
|
+
});
|
|
130
|
+
}, [src]);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<style>{`
|
|
135
|
+
@keyframes slop-shimmer {
|
|
136
|
+
0% {
|
|
137
|
+
background-position: 200% 0;
|
|
138
|
+
}
|
|
139
|
+
100% {
|
|
140
|
+
background-position: -200% 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
@keyframes slop-spin {
|
|
144
|
+
from {
|
|
145
|
+
transform: rotate(0deg);
|
|
146
|
+
}
|
|
147
|
+
to {
|
|
148
|
+
transform: rotate(360deg);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`}</style>
|
|
152
|
+
<div className={className} style={props.style}>
|
|
153
|
+
<div
|
|
154
|
+
className="slop-wrapper relative overflow-hidden w-full h-full"
|
|
155
|
+
style={{
|
|
156
|
+
aspectRatio: String(aspectRatio).replace(":", "/"),
|
|
157
|
+
backgroundColor: "var(--muted, #f3f4f6)",
|
|
158
|
+
position: "relative",
|
|
159
|
+
overflow: "hidden",
|
|
160
|
+
width: "100%",
|
|
161
|
+
height: "100%",
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{/* Loading UI */}
|
|
165
|
+
{loader ? (
|
|
166
|
+
<div
|
|
167
|
+
className={cn(
|
|
168
|
+
"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
|
|
169
|
+
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
170
|
+
)}
|
|
171
|
+
style={{
|
|
172
|
+
position: "absolute",
|
|
173
|
+
top: 0,
|
|
174
|
+
left: 0,
|
|
175
|
+
right: 0,
|
|
176
|
+
bottom: 0,
|
|
177
|
+
zIndex: 10,
|
|
178
|
+
display: "flex",
|
|
179
|
+
alignItems: "center",
|
|
180
|
+
justifyContent: "center",
|
|
181
|
+
opacity: isLoading ? 1 : 0,
|
|
182
|
+
pointerEvents: isLoading ? "auto" : "none",
|
|
183
|
+
transition: "opacity 300ms ease-in-out",
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{loader}
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<>
|
|
190
|
+
{/* Loading overlay */}
|
|
191
|
+
<div
|
|
192
|
+
className={cn(
|
|
193
|
+
"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
|
|
194
|
+
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
195
|
+
)}
|
|
196
|
+
style={{
|
|
197
|
+
position: "absolute",
|
|
198
|
+
top: 0,
|
|
199
|
+
left: 0,
|
|
200
|
+
right: 0,
|
|
201
|
+
bottom: 0,
|
|
202
|
+
zIndex: 10,
|
|
203
|
+
display: "flex",
|
|
204
|
+
alignItems: "center",
|
|
205
|
+
justifyContent: "center",
|
|
206
|
+
backgroundColor: "var(--muted, #f3f4f6)",
|
|
207
|
+
opacity: isLoading ? 1 : 0,
|
|
208
|
+
pointerEvents: isLoading ? "auto" : "none",
|
|
209
|
+
transition: "opacity 300ms ease-in-out",
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<div
|
|
213
|
+
className="flex flex-col items-center gap-2"
|
|
214
|
+
style={{
|
|
215
|
+
display: "flex",
|
|
216
|
+
flexDirection: "column",
|
|
217
|
+
alignItems: "center",
|
|
218
|
+
gap: "0.5rem",
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<svg
|
|
222
|
+
className="spinner size-6 text-muted-foreground"
|
|
223
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
224
|
+
fill="none"
|
|
225
|
+
viewBox="0 0 24 24"
|
|
226
|
+
style={{
|
|
227
|
+
width: "24px",
|
|
228
|
+
height: "24px",
|
|
229
|
+
color: "var(--muted-foreground, #6b7280)",
|
|
230
|
+
animation: "slop-spin 1s linear infinite",
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
<circle
|
|
234
|
+
className="opacity-25"
|
|
235
|
+
cx="12"
|
|
236
|
+
cy="12"
|
|
237
|
+
r="10"
|
|
238
|
+
stroke="currentColor"
|
|
239
|
+
strokeWidth="4"
|
|
240
|
+
style={{ opacity: 0.25 }}
|
|
241
|
+
/>
|
|
242
|
+
<path
|
|
243
|
+
className="opacity-75"
|
|
244
|
+
fill="currentColor"
|
|
245
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
246
|
+
style={{ opacity: 0.75 }}
|
|
247
|
+
/>
|
|
248
|
+
</svg>
|
|
249
|
+
<span
|
|
250
|
+
className="text-xs text-muted-foreground"
|
|
251
|
+
style={{
|
|
252
|
+
fontSize: "0.75rem",
|
|
253
|
+
color: "var(--muted-foreground, #6b7280)",
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
Loading...
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Shimmer effect underneath */}
|
|
262
|
+
<div
|
|
263
|
+
className={cn(
|
|
264
|
+
"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
|
|
265
|
+
isLoading ? "opacity-100" : "opacity-0",
|
|
266
|
+
)}
|
|
267
|
+
style={{
|
|
268
|
+
position: "absolute",
|
|
269
|
+
top: 0,
|
|
270
|
+
left: 0,
|
|
271
|
+
right: 0,
|
|
272
|
+
bottom: 0,
|
|
273
|
+
background:
|
|
274
|
+
"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
|
|
275
|
+
backgroundSize: "200% 100%",
|
|
276
|
+
animation: "slop-shimmer 2s ease-in-out infinite",
|
|
277
|
+
opacity: isLoading ? 1 : 0,
|
|
278
|
+
transition: "opacity 300ms",
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* Video */}
|
|
285
|
+
<video
|
|
286
|
+
key={src}
|
|
287
|
+
ref={videoRef}
|
|
288
|
+
src={src}
|
|
289
|
+
autoPlay={autoPlay}
|
|
290
|
+
loop={loop}
|
|
291
|
+
muted={muted}
|
|
292
|
+
playsInline={playsInline}
|
|
293
|
+
onLoadedData={() => setIsLoading(false)}
|
|
294
|
+
onError={() => setIsLoading(false)}
|
|
295
|
+
className={cn(
|
|
296
|
+
"h-full w-full object-cover transition-opacity duration-500",
|
|
297
|
+
isLoading ? "opacity-0" : "opacity-100",
|
|
298
|
+
)}
|
|
299
|
+
style={{
|
|
300
|
+
height: "100%",
|
|
301
|
+
width: "100%",
|
|
302
|
+
objectFit: "cover",
|
|
303
|
+
transition: "opacity 500ms",
|
|
304
|
+
opacity: isLoading ? 0 : 1,
|
|
305
|
+
}}
|
|
306
|
+
{...props}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</>
|
|
311
|
+
);
|
|
312
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
export { SlopImage } from
|
|
1
|
+
export { SlopImage, type SlopImageProps } from "./components/SlopImage";
|
|
2
|
+
export { SlopVideo, type SlopVideoProps } from "./components/SlopVideo";
|
|
3
|
+
export type {
|
|
4
|
+
ImageAspectRatio,
|
|
5
|
+
VideoAspectRatio,
|
|
6
|
+
SlopImageOptions,
|
|
7
|
+
SlopVideoOptions,
|
|
8
|
+
} from "@slopmachine/core";
|
package/tsconfig.json
CHANGED
package/tsup.config.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
-
entry: [
|
|
5
|
-
format: [
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["cjs", "esm"],
|
|
6
6
|
dts: true,
|
|
7
|
+
sourcemap: true,
|
|
7
8
|
clean: true,
|
|
8
|
-
external: [
|
|
9
|
+
external: ["react", "react-dom"],
|
|
9
10
|
});
|