@slopmachine/react 0.7.0 → 0.8.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/CHANGELOG.md +28 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -7
- package/Agents.md +0 -36
- package/src/components/ErrorOverlay.tsx +0 -91
- package/src/components/SlopImage.tsx +0 -376
- package/src/components/SlopText.tsx +0 -276
- package/src/components/SlopVideo.tsx +0 -373
- package/src/index.ts +0 -23
- package/tsconfig.json +0 -29
- package/tsup.config.ts +0 -10
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
buildTextUrl,
|
|
4
|
-
renderMarkdown,
|
|
5
|
-
type SlopTextOptions,
|
|
6
|
-
} from "@slopmachine/core";
|
|
7
|
-
|
|
8
|
-
export interface SlopTextProps
|
|
9
|
-
extends
|
|
10
|
-
Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
|
|
11
|
-
SlopTextOptions {
|
|
12
|
-
/**
|
|
13
|
-
* Optional loading component to show while the text is being generated
|
|
14
|
-
*/
|
|
15
|
-
fallback?: React.ReactNode;
|
|
16
|
-
/**
|
|
17
|
-
* Optional error component to show if the text generation fails
|
|
18
|
-
*/
|
|
19
|
-
errorFallback?: React.ReactNode;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Renders text generated by Slop Machine.
|
|
24
|
-
*
|
|
25
|
-
* This component automatically constructs the correct URL for the Slop Machine API,
|
|
26
|
-
* fetches the generated text, and displays it with optional loading and error states.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```tsx
|
|
30
|
-
* // Using a Bucket
|
|
31
|
-
* <SlopText
|
|
32
|
-
* bucketId="my-text-bucket"
|
|
33
|
-
* fallback={<div>Loading story...</div>}
|
|
34
|
-
* errorFallback={<div>Failed to load story</div>}
|
|
35
|
-
* className="text-lg text-gray-800"
|
|
36
|
-
* />
|
|
37
|
-
*
|
|
38
|
-
* // Using a Pipeline with runtime prompt
|
|
39
|
-
* <SlopText
|
|
40
|
-
* pipelineId="pipe_live_abc123"
|
|
41
|
-
* prompt="Write a whimsical haiku about quantum physics"
|
|
42
|
-
* metadata={{ userId: "123" }}
|
|
43
|
-
* />
|
|
44
|
-
* ```
|
|
45
|
-
*
|
|
46
|
-
* @version 0.7.0
|
|
47
|
-
*/
|
|
48
|
-
export const SlopText = React.forwardRef<HTMLDivElement, SlopTextProps>(
|
|
49
|
-
(
|
|
50
|
-
{
|
|
51
|
-
bucketId,
|
|
52
|
-
pipelineId,
|
|
53
|
-
siloId,
|
|
54
|
-
prompt,
|
|
55
|
-
metadata,
|
|
56
|
-
version,
|
|
57
|
-
resultId,
|
|
58
|
-
model,
|
|
59
|
-
variables,
|
|
60
|
-
baseUrl,
|
|
61
|
-
attachments,
|
|
62
|
-
fallback,
|
|
63
|
-
errorFallback,
|
|
64
|
-
...props
|
|
65
|
-
},
|
|
66
|
-
ref,
|
|
67
|
-
) => {
|
|
68
|
-
const [text, setText] = useState<string | null>(null);
|
|
69
|
-
const [loading, setLoading] = useState(true);
|
|
70
|
-
const [error, setError] = useState<Error | null>(null);
|
|
71
|
-
|
|
72
|
-
const rawUrl = useMemo(
|
|
73
|
-
() =>
|
|
74
|
-
buildTextUrl({
|
|
75
|
-
bucketId,
|
|
76
|
-
pipelineId,
|
|
77
|
-
siloId,
|
|
78
|
-
prompt,
|
|
79
|
-
metadata,
|
|
80
|
-
version,
|
|
81
|
-
resultId,
|
|
82
|
-
model,
|
|
83
|
-
variables,
|
|
84
|
-
baseUrl,
|
|
85
|
-
attachments,
|
|
86
|
-
}),
|
|
87
|
-
[
|
|
88
|
-
bucketId,
|
|
89
|
-
pipelineId,
|
|
90
|
-
siloId,
|
|
91
|
-
prompt,
|
|
92
|
-
JSON.stringify(metadata),
|
|
93
|
-
version,
|
|
94
|
-
resultId,
|
|
95
|
-
model,
|
|
96
|
-
JSON.stringify(variables),
|
|
97
|
-
baseUrl,
|
|
98
|
-
JSON.stringify(attachments),
|
|
99
|
-
],
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
let isMounted = true;
|
|
104
|
-
|
|
105
|
-
const fetchText = async () => {
|
|
106
|
-
try {
|
|
107
|
-
setLoading(true);
|
|
108
|
-
setError(null);
|
|
109
|
-
|
|
110
|
-
// Fetch the URL to get the content, following redirects automatically
|
|
111
|
-
const response = await fetch(rawUrl, { cache: "no-store" });
|
|
112
|
-
if (!response.ok) {
|
|
113
|
-
let errorMessage = response.statusText;
|
|
114
|
-
try {
|
|
115
|
-
const errorText = await response.text();
|
|
116
|
-
const errorData = JSON.parse(errorText);
|
|
117
|
-
if (errorData.error) {
|
|
118
|
-
errorMessage = errorData.error;
|
|
119
|
-
}
|
|
120
|
-
} catch (e) {
|
|
121
|
-
// Ignore JSON parse errors if the response isn't JSON
|
|
122
|
-
}
|
|
123
|
-
throw new Error(`Failed to fetch text: ${errorMessage}`);
|
|
124
|
-
}
|
|
125
|
-
const content = await response.text();
|
|
126
|
-
if (isMounted) {
|
|
127
|
-
setText(content);
|
|
128
|
-
}
|
|
129
|
-
} catch (err) {
|
|
130
|
-
if (isMounted) {
|
|
131
|
-
const finalError =
|
|
132
|
-
err instanceof Error ? err : new Error("Failed to generate text");
|
|
133
|
-
console.error(finalError);
|
|
134
|
-
setError(finalError);
|
|
135
|
-
}
|
|
136
|
-
} finally {
|
|
137
|
-
if (isMounted) {
|
|
138
|
-
setLoading(false);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
fetchText();
|
|
144
|
-
|
|
145
|
-
return () => {
|
|
146
|
-
isMounted = false;
|
|
147
|
-
};
|
|
148
|
-
}, [rawUrl]);
|
|
149
|
-
|
|
150
|
-
if (error && errorFallback) {
|
|
151
|
-
return <>{errorFallback}</>;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (loading) {
|
|
155
|
-
if (fallback) {
|
|
156
|
-
return <>{fallback}</>;
|
|
157
|
-
}
|
|
158
|
-
return (
|
|
159
|
-
<div className={props.className} style={props.style}>
|
|
160
|
-
<style>{`
|
|
161
|
-
@keyframes slop-shimmer {
|
|
162
|
-
0% { background-position: 200% 0; }
|
|
163
|
-
100% { background-position: -200% 0; }
|
|
164
|
-
}
|
|
165
|
-
@keyframes slop-spin {
|
|
166
|
-
from { transform: rotate(0deg); }
|
|
167
|
-
to { transform: rotate(360deg); }
|
|
168
|
-
}
|
|
169
|
-
`}</style>
|
|
170
|
-
<div
|
|
171
|
-
className="slop-wrapper relative overflow-hidden w-full"
|
|
172
|
-
style={{
|
|
173
|
-
position: "relative",
|
|
174
|
-
overflow: "hidden",
|
|
175
|
-
width: "100%",
|
|
176
|
-
minHeight: "100px",
|
|
177
|
-
}}
|
|
178
|
-
>
|
|
179
|
-
{/* Loading overlay */}
|
|
180
|
-
<div
|
|
181
|
-
className="absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300"
|
|
182
|
-
style={{
|
|
183
|
-
position: "absolute",
|
|
184
|
-
top: 0,
|
|
185
|
-
left: 0,
|
|
186
|
-
right: 0,
|
|
187
|
-
bottom: 0,
|
|
188
|
-
zIndex: 10,
|
|
189
|
-
display: "flex",
|
|
190
|
-
alignItems: "center",
|
|
191
|
-
justifyContent: "center",
|
|
192
|
-
backgroundColor: "var(--muted, #f3f4f6)",
|
|
193
|
-
}}
|
|
194
|
-
>
|
|
195
|
-
<div
|
|
196
|
-
className="flex flex-col items-center gap-2"
|
|
197
|
-
style={{
|
|
198
|
-
display: "flex",
|
|
199
|
-
flexDirection: "column",
|
|
200
|
-
alignItems: "center",
|
|
201
|
-
gap: "0.5rem",
|
|
202
|
-
}}
|
|
203
|
-
>
|
|
204
|
-
<svg
|
|
205
|
-
className="spinner size-6 text-muted-foreground"
|
|
206
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
207
|
-
fill="none"
|
|
208
|
-
viewBox="0 0 24 24"
|
|
209
|
-
style={{
|
|
210
|
-
width: "24px",
|
|
211
|
-
height: "24px",
|
|
212
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
213
|
-
animation: "slop-spin 1s linear infinite",
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
216
|
-
<circle
|
|
217
|
-
className="opacity-25"
|
|
218
|
-
cx="12"
|
|
219
|
-
cy="12"
|
|
220
|
-
r="10"
|
|
221
|
-
stroke="currentColor"
|
|
222
|
-
strokeWidth="4"
|
|
223
|
-
style={{ opacity: 0.25 }}
|
|
224
|
-
/>
|
|
225
|
-
<path
|
|
226
|
-
className="opacity-75"
|
|
227
|
-
fill="currentColor"
|
|
228
|
-
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"
|
|
229
|
-
style={{ opacity: 0.75 }}
|
|
230
|
-
/>
|
|
231
|
-
</svg>
|
|
232
|
-
<span
|
|
233
|
-
className="text-xs text-muted-foreground"
|
|
234
|
-
style={{
|
|
235
|
-
fontSize: "0.75rem",
|
|
236
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
Loading...
|
|
240
|
-
</span>
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
{/* Shimmer effect underneath */}
|
|
245
|
-
<div
|
|
246
|
-
className="shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted"
|
|
247
|
-
style={{
|
|
248
|
-
position: "absolute",
|
|
249
|
-
top: 0,
|
|
250
|
-
left: 0,
|
|
251
|
-
right: 0,
|
|
252
|
-
bottom: 0,
|
|
253
|
-
background:
|
|
254
|
-
"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
|
|
255
|
-
backgroundSize: "200% 100%",
|
|
256
|
-
animation: "slop-shimmer 2s ease-in-out infinite",
|
|
257
|
-
}}
|
|
258
|
-
/>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return (
|
|
265
|
-
<div
|
|
266
|
-
ref={ref}
|
|
267
|
-
{...props}
|
|
268
|
-
dangerouslySetInnerHTML={{
|
|
269
|
-
__html: text ? renderMarkdown(text) : "",
|
|
270
|
-
}}
|
|
271
|
-
/>
|
|
272
|
-
);
|
|
273
|
-
},
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
SlopText.displayName = "SlopText";
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { clsx, type ClassValue } from "clsx";
|
|
3
|
-
import { twMerge } from "tailwind-merge";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
buildVideoUrl,
|
|
7
|
-
createRenderUrlMonitor,
|
|
8
|
-
type SlopMachineError,
|
|
9
|
-
type SlopVideoOptions,
|
|
10
|
-
type VideoAspectRatio,
|
|
11
|
-
} from "@slopmachine/core";
|
|
12
|
-
import { ErrorOverlay, type SlopErrorFallback } from "./ErrorOverlay";
|
|
13
|
-
|
|
14
|
-
// Utility for Tailwind classes
|
|
15
|
-
function cn(...inputs: ClassValue[]) {
|
|
16
|
-
return twMerge(clsx(inputs));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface SlopVideoProps
|
|
20
|
-
extends
|
|
21
|
-
Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "src">,
|
|
22
|
-
Omit<SlopVideoOptions, "aspectRatio"> {
|
|
23
|
-
/**
|
|
24
|
-
* The aspect ratio of the generated video. Defaults to "16:9".
|
|
25
|
-
*/
|
|
26
|
-
aspectRatio?: VideoAspectRatio;
|
|
27
|
-
/**
|
|
28
|
-
* Custom React node to display while the video is loading.
|
|
29
|
-
* If not provided, a default spinner and shimmer effect will be shown.
|
|
30
|
-
*/
|
|
31
|
-
loader?: React.ReactNode;
|
|
32
|
-
/**
|
|
33
|
-
* Content to display if the video fails to generate or load.
|
|
34
|
-
* Pass a node, or a function that receives the `SlopMachineError` and returns a node.
|
|
35
|
-
* If not provided, a default error message is shown in place of the video.
|
|
36
|
-
*/
|
|
37
|
-
errorFallback?: SlopErrorFallback;
|
|
38
|
-
/**
|
|
39
|
-
* Called once per URL when the video fails to generate or load.
|
|
40
|
-
* `error.status` holds the HTTP status when the API reported the failure.
|
|
41
|
-
*/
|
|
42
|
-
onGenerationError?: (error: SlopMachineError) => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Renders a video generated by Slop Machine.
|
|
47
|
-
*
|
|
48
|
-
* This component automatically constructs the correct URL for the Slop Machine API,
|
|
49
|
-
* displays a loading skeleton or custom loader while fetching, and handles errors.
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```tsx
|
|
53
|
-
* // Using a Bucket
|
|
54
|
-
* <SlopVideo
|
|
55
|
-
* bucketId="my-bucket"
|
|
56
|
-
* resultId="some-result"
|
|
57
|
-
* aspectRatio="16:9"
|
|
58
|
-
* className="rounded-lg"
|
|
59
|
-
* autoPlay
|
|
60
|
-
* loop
|
|
61
|
-
* muted
|
|
62
|
-
* />
|
|
63
|
-
*
|
|
64
|
-
* // Using a Pipeline with runtime prompt
|
|
65
|
-
* <SlopVideo
|
|
66
|
-
* pipelineId="pipe_live_abc123"
|
|
67
|
-
* prompt="Drone shot of a cyberpunk city in rain"
|
|
68
|
-
* aspectRatio="16:9"
|
|
69
|
-
* metadata={{ campaign: "spring_launch" }}
|
|
70
|
-
* />
|
|
71
|
-
* ```
|
|
72
|
-
*
|
|
73
|
-
* @version 0.7.0
|
|
74
|
-
*/
|
|
75
|
-
export const SlopVideo: React.FC<SlopVideoProps> = ({
|
|
76
|
-
bucketId,
|
|
77
|
-
pipelineId,
|
|
78
|
-
siloId,
|
|
79
|
-
prompt,
|
|
80
|
-
metadata,
|
|
81
|
-
className,
|
|
82
|
-
aspectRatio = "16:9",
|
|
83
|
-
version,
|
|
84
|
-
resultId,
|
|
85
|
-
variables = {},
|
|
86
|
-
duration,
|
|
87
|
-
baseUrl,
|
|
88
|
-
original,
|
|
89
|
-
attachments,
|
|
90
|
-
loader,
|
|
91
|
-
errorFallback,
|
|
92
|
-
onGenerationError,
|
|
93
|
-
autoPlay = true,
|
|
94
|
-
loop = true,
|
|
95
|
-
muted = true,
|
|
96
|
-
playsInline = true,
|
|
97
|
-
onLoadedData,
|
|
98
|
-
onError,
|
|
99
|
-
...props
|
|
100
|
-
}) => {
|
|
101
|
-
// Construct API URL
|
|
102
|
-
const rawSrc = useMemo(
|
|
103
|
-
() =>
|
|
104
|
-
buildVideoUrl({
|
|
105
|
-
bucketId,
|
|
106
|
-
pipelineId,
|
|
107
|
-
siloId,
|
|
108
|
-
prompt,
|
|
109
|
-
metadata,
|
|
110
|
-
aspectRatio,
|
|
111
|
-
version,
|
|
112
|
-
resultId,
|
|
113
|
-
variables,
|
|
114
|
-
duration,
|
|
115
|
-
baseUrl,
|
|
116
|
-
original,
|
|
117
|
-
attachments,
|
|
118
|
-
}),
|
|
119
|
-
[
|
|
120
|
-
bucketId,
|
|
121
|
-
pipelineId,
|
|
122
|
-
siloId,
|
|
123
|
-
prompt,
|
|
124
|
-
metadata,
|
|
125
|
-
aspectRatio,
|
|
126
|
-
version,
|
|
127
|
-
resultId,
|
|
128
|
-
variables,
|
|
129
|
-
duration,
|
|
130
|
-
baseUrl,
|
|
131
|
-
original,
|
|
132
|
-
attachments,
|
|
133
|
-
],
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const [src, setSrc] = useState(rawSrc);
|
|
137
|
-
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
const timer = setTimeout(() => {
|
|
140
|
-
setSrc(rawSrc);
|
|
141
|
-
}, 100);
|
|
142
|
-
return () => clearTimeout(timer);
|
|
143
|
-
}, [rawSrc]);
|
|
144
|
-
|
|
145
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
146
|
-
const [error, setError] = useState<SlopMachineError | null>(null);
|
|
147
|
-
const [currentSrc, setCurrentSrc] = useState(src);
|
|
148
|
-
const videoRef = useRef<HTMLVideoElement>(null);
|
|
149
|
-
if (src !== currentSrc) {
|
|
150
|
-
setCurrentSrc(src);
|
|
151
|
-
setIsLoading(true);
|
|
152
|
-
setError(null);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Keep the latest callback without re-running effects when it changes identity
|
|
156
|
-
const onGenerationErrorRef = useRef(onGenerationError);
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
onGenerationErrorRef.current = onGenerationError;
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Shares one URL check between the HEAD probe and the media element's error event
|
|
162
|
-
const monitor = useMemo(
|
|
163
|
-
() =>
|
|
164
|
-
createRenderUrlMonitor((err) => {
|
|
165
|
-
console.error("SlopVideo error:", err.status ?? "", err.message);
|
|
166
|
-
setIsLoading(false);
|
|
167
|
-
setError(err);
|
|
168
|
-
onGenerationErrorRef.current?.(err);
|
|
169
|
-
}),
|
|
170
|
-
[],
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
if (!src) return;
|
|
175
|
-
monitor.watch(src);
|
|
176
|
-
return () => monitor.stop();
|
|
177
|
-
}, [src, monitor]);
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<>
|
|
181
|
-
<style>{`
|
|
182
|
-
@keyframes slop-shimmer {
|
|
183
|
-
0% {
|
|
184
|
-
background-position: 200% 0;
|
|
185
|
-
}
|
|
186
|
-
100% {
|
|
187
|
-
background-position: -200% 0;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
@keyframes slop-spin {
|
|
191
|
-
from {
|
|
192
|
-
transform: rotate(0deg);
|
|
193
|
-
}
|
|
194
|
-
to {
|
|
195
|
-
transform: rotate(360deg);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
`}</style>
|
|
199
|
-
<div className={className} style={props.style}>
|
|
200
|
-
<div
|
|
201
|
-
className="slop-wrapper relative overflow-hidden w-full h-full"
|
|
202
|
-
style={{
|
|
203
|
-
aspectRatio: String(aspectRatio).replace(":", "/"),
|
|
204
|
-
position: "relative",
|
|
205
|
-
overflow: "hidden",
|
|
206
|
-
width: "100%",
|
|
207
|
-
height: "100%",
|
|
208
|
-
}}
|
|
209
|
-
>
|
|
210
|
-
{/* Loading UI */}
|
|
211
|
-
{loader ? (
|
|
212
|
-
<div
|
|
213
|
-
className={cn(
|
|
214
|
-
"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
|
|
215
|
-
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
216
|
-
)}
|
|
217
|
-
style={{
|
|
218
|
-
position: "absolute",
|
|
219
|
-
top: 0,
|
|
220
|
-
left: 0,
|
|
221
|
-
right: 0,
|
|
222
|
-
bottom: 0,
|
|
223
|
-
zIndex: 10,
|
|
224
|
-
display: "flex",
|
|
225
|
-
alignItems: "center",
|
|
226
|
-
justifyContent: "center",
|
|
227
|
-
opacity: isLoading ? 1 : 0,
|
|
228
|
-
pointerEvents: isLoading ? "auto" : "none",
|
|
229
|
-
transition: "opacity 300ms ease-in-out",
|
|
230
|
-
}}
|
|
231
|
-
>
|
|
232
|
-
{loader}
|
|
233
|
-
</div>
|
|
234
|
-
) : (
|
|
235
|
-
<>
|
|
236
|
-
{/* Loading overlay */}
|
|
237
|
-
<div
|
|
238
|
-
className={cn(
|
|
239
|
-
"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
|
|
240
|
-
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
241
|
-
)}
|
|
242
|
-
style={{
|
|
243
|
-
position: "absolute",
|
|
244
|
-
top: 0,
|
|
245
|
-
left: 0,
|
|
246
|
-
right: 0,
|
|
247
|
-
bottom: 0,
|
|
248
|
-
zIndex: 10,
|
|
249
|
-
display: "flex",
|
|
250
|
-
alignItems: "center",
|
|
251
|
-
justifyContent: "center",
|
|
252
|
-
backgroundColor: "var(--muted, #f3f4f6)",
|
|
253
|
-
opacity: isLoading ? 1 : 0,
|
|
254
|
-
pointerEvents: isLoading ? "auto" : "none",
|
|
255
|
-
transition: "opacity 300ms ease-in-out",
|
|
256
|
-
}}
|
|
257
|
-
>
|
|
258
|
-
<div
|
|
259
|
-
className="flex flex-col items-center gap-2"
|
|
260
|
-
style={{
|
|
261
|
-
display: "flex",
|
|
262
|
-
flexDirection: "column",
|
|
263
|
-
alignItems: "center",
|
|
264
|
-
gap: "0.5rem",
|
|
265
|
-
}}
|
|
266
|
-
>
|
|
267
|
-
<svg
|
|
268
|
-
className="spinner size-6 text-muted-foreground"
|
|
269
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
270
|
-
fill="none"
|
|
271
|
-
viewBox="0 0 24 24"
|
|
272
|
-
style={{
|
|
273
|
-
width: "24px",
|
|
274
|
-
height: "24px",
|
|
275
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
276
|
-
animation: "slop-spin 1s linear infinite",
|
|
277
|
-
}}
|
|
278
|
-
>
|
|
279
|
-
<circle
|
|
280
|
-
className="opacity-25"
|
|
281
|
-
cx="12"
|
|
282
|
-
cy="12"
|
|
283
|
-
r="10"
|
|
284
|
-
stroke="currentColor"
|
|
285
|
-
strokeWidth="4"
|
|
286
|
-
style={{ opacity: 0.25 }}
|
|
287
|
-
/>
|
|
288
|
-
<path
|
|
289
|
-
className="opacity-75"
|
|
290
|
-
fill="currentColor"
|
|
291
|
-
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"
|
|
292
|
-
style={{ opacity: 0.75 }}
|
|
293
|
-
/>
|
|
294
|
-
</svg>
|
|
295
|
-
<span
|
|
296
|
-
className="text-xs text-muted-foreground"
|
|
297
|
-
style={{
|
|
298
|
-
fontSize: "0.75rem",
|
|
299
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
300
|
-
}}
|
|
301
|
-
>
|
|
302
|
-
Loading...
|
|
303
|
-
</span>
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
{/* Shimmer effect underneath */}
|
|
308
|
-
<div
|
|
309
|
-
className={cn(
|
|
310
|
-
"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
|
|
311
|
-
isLoading ? "opacity-100" : "opacity-0",
|
|
312
|
-
)}
|
|
313
|
-
style={{
|
|
314
|
-
position: "absolute",
|
|
315
|
-
top: 0,
|
|
316
|
-
left: 0,
|
|
317
|
-
right: 0,
|
|
318
|
-
bottom: 0,
|
|
319
|
-
background:
|
|
320
|
-
"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
|
|
321
|
-
backgroundSize: "200% 100%",
|
|
322
|
-
animation: "slop-shimmer 2s ease-in-out infinite",
|
|
323
|
-
opacity: isLoading ? 1 : 0,
|
|
324
|
-
transition: "opacity 300ms",
|
|
325
|
-
}}
|
|
326
|
-
/>
|
|
327
|
-
</>
|
|
328
|
-
)}
|
|
329
|
-
|
|
330
|
-
{error && (
|
|
331
|
-
<ErrorOverlay
|
|
332
|
-
error={error}
|
|
333
|
-
errorFallback={errorFallback}
|
|
334
|
-
label="Failed to load video"
|
|
335
|
-
/>
|
|
336
|
-
)}
|
|
337
|
-
|
|
338
|
-
{/* Video */}
|
|
339
|
-
<video
|
|
340
|
-
key={src}
|
|
341
|
-
ref={videoRef}
|
|
342
|
-
src={src}
|
|
343
|
-
autoPlay={autoPlay}
|
|
344
|
-
loop={loop}
|
|
345
|
-
muted={muted}
|
|
346
|
-
playsInline={playsInline}
|
|
347
|
-
{...props}
|
|
348
|
-
onLoadedData={(e) => {
|
|
349
|
-
setIsLoading(false);
|
|
350
|
-
onLoadedData?.(e);
|
|
351
|
-
}}
|
|
352
|
-
onError={(e) => {
|
|
353
|
-
monitor.mediaFailed(src, "Failed to load video");
|
|
354
|
-
onError?.(e);
|
|
355
|
-
}}
|
|
356
|
-
aria-hidden={error ? true : props["aria-hidden"]}
|
|
357
|
-
className={cn(
|
|
358
|
-
"h-full w-full object-cover transition-opacity duration-500",
|
|
359
|
-
isLoading || error ? "opacity-0" : "opacity-100",
|
|
360
|
-
)}
|
|
361
|
-
style={{
|
|
362
|
-
height: "100%",
|
|
363
|
-
width: "100%",
|
|
364
|
-
objectFit: "cover",
|
|
365
|
-
transition: "opacity 500ms",
|
|
366
|
-
opacity: isLoading || error ? 0 : 1,
|
|
367
|
-
}}
|
|
368
|
-
/>
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
</>
|
|
372
|
-
);
|
|
373
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export { SlopImage, type SlopImageProps } from "./components/SlopImage";
|
|
2
|
-
export { SlopVideo, type SlopVideoProps } from "./components/SlopVideo";
|
|
3
|
-
export { SlopText, type SlopTextProps } from "./components/SlopText";
|
|
4
|
-
export type { SlopErrorFallback } from "./components/ErrorOverlay";
|
|
5
|
-
|
|
6
|
-
export type {
|
|
7
|
-
ImageAspectRatio,
|
|
8
|
-
VideoAspectRatio,
|
|
9
|
-
SlopImageOptions,
|
|
10
|
-
SlopVideoOptions,
|
|
11
|
-
SlopTextOptions,
|
|
12
|
-
ResultRating,
|
|
13
|
-
RateResultOptions,
|
|
14
|
-
RateResultResponse,
|
|
15
|
-
} from "@slopmachine/core";
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
preloadImage,
|
|
19
|
-
preloadVideo,
|
|
20
|
-
preloadText,
|
|
21
|
-
rateResult,
|
|
22
|
-
SlopMachineError,
|
|
23
|
-
} from "@slopmachine/core";
|
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"useDefineForClassFields": true,
|
|
5
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
|
|
9
|
-
/* Bundler mode */
|
|
10
|
-
"moduleResolution": "bundler",
|
|
11
|
-
"allowImportingTsExtensions": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"isolatedModules": true,
|
|
14
|
-
"noEmit": true,
|
|
15
|
-
"jsx": "react-jsx",
|
|
16
|
-
|
|
17
|
-
/* Linting */
|
|
18
|
-
"strict": true,
|
|
19
|
-
"noUnusedLocals": true,
|
|
20
|
-
"noUnusedParameters": true,
|
|
21
|
-
"noFallthroughCasesInSwitch": true,
|
|
22
|
-
|
|
23
|
-
"declaration": true,
|
|
24
|
-
"declarationMap": true,
|
|
25
|
-
"baseUrl": "."
|
|
26
|
-
},
|
|
27
|
-
"include": ["src"],
|
|
28
|
-
"exclude": ["node_modules", "dist"]
|
|
29
|
-
}
|