@slopmachine/react 0.6.0 → 0.8.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/CHANGELOG.md +36 -0
- package/dist/index.d.mts +34 -6
- package/dist/index.d.ts +34 -6
- package/dist/index.js +186 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +199 -100
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -7
- package/Agents.md +0 -36
- package/src/components/SlopImage.tsx +0 -353
- package/src/components/SlopText.tsx +0 -276
- package/src/components/SlopVideo.tsx +0 -350
- package/src/index.ts +0 -21
- package/tsconfig.json +0 -29
- package/tsup.config.ts +0 -10
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import React, { useMemo, useState } from "react";
|
|
2
|
-
import { clsx, type ClassValue } from "clsx";
|
|
3
|
-
import { twMerge } from "tailwind-merge";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
buildImageUrl,
|
|
7
|
-
type SlopImageOptions,
|
|
8
|
-
type ImageAspectRatio,
|
|
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 SlopImageProps
|
|
17
|
-
extends
|
|
18
|
-
Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">,
|
|
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
|
-
*/
|
|
28
|
-
loader?: React.ReactNode;
|
|
29
|
-
/**
|
|
30
|
-
* How the image should be resized to fit its container. Defaults to "cover".
|
|
31
|
-
*/
|
|
32
|
-
objectFit?: React.CSSProperties["objectFit"];
|
|
33
|
-
/**
|
|
34
|
-
* Additional CSS classes to apply directly to the `<img />` element.
|
|
35
|
-
*/
|
|
36
|
-
imageClassName?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Renders an image generated by Slop Machine.
|
|
41
|
-
*
|
|
42
|
-
* This component automatically constructs the correct URL for the Slop Machine API,
|
|
43
|
-
* displays a loading skeleton or custom loader while fetching, and handles errors.
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```tsx
|
|
47
|
-
* // Using a Bucket
|
|
48
|
-
* <SlopImage
|
|
49
|
-
* bucketId="my-bucket"
|
|
50
|
-
* resultId="some-result"
|
|
51
|
-
* aspectRatio="16:9"
|
|
52
|
-
* className="rounded-lg"
|
|
53
|
-
* objectFit="cover"
|
|
54
|
-
* />
|
|
55
|
-
*
|
|
56
|
-
* // Using a Pipeline with runtime prompt
|
|
57
|
-
* <SlopImage
|
|
58
|
-
* pipelineId="pipe_live_abc123"
|
|
59
|
-
* prompt="1960s retro robot eating pizza on Mars"
|
|
60
|
-
* aspectRatio="16:9"
|
|
61
|
-
* metadata={{ userId: "123" }}
|
|
62
|
-
* />
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* @version 0.6.0
|
|
66
|
-
*/
|
|
67
|
-
export const SlopImage: React.FC<SlopImageProps> = ({
|
|
68
|
-
bucketId,
|
|
69
|
-
pipelineId,
|
|
70
|
-
siloId,
|
|
71
|
-
prompt,
|
|
72
|
-
metadata,
|
|
73
|
-
className,
|
|
74
|
-
aspectRatio = "1:1",
|
|
75
|
-
model,
|
|
76
|
-
version,
|
|
77
|
-
resultId,
|
|
78
|
-
variables = {},
|
|
79
|
-
baseUrl,
|
|
80
|
-
original,
|
|
81
|
-
attachments,
|
|
82
|
-
loader,
|
|
83
|
-
objectFit = "cover",
|
|
84
|
-
imageClassName,
|
|
85
|
-
style,
|
|
86
|
-
onLoad,
|
|
87
|
-
onError,
|
|
88
|
-
...props
|
|
89
|
-
}) => {
|
|
90
|
-
// Construct API URL
|
|
91
|
-
const rawSrc = useMemo(
|
|
92
|
-
() =>
|
|
93
|
-
buildImageUrl({
|
|
94
|
-
bucketId,
|
|
95
|
-
pipelineId,
|
|
96
|
-
siloId,
|
|
97
|
-
prompt,
|
|
98
|
-
metadata,
|
|
99
|
-
aspectRatio,
|
|
100
|
-
model,
|
|
101
|
-
version,
|
|
102
|
-
resultId,
|
|
103
|
-
variables,
|
|
104
|
-
baseUrl,
|
|
105
|
-
original,
|
|
106
|
-
attachments,
|
|
107
|
-
}),
|
|
108
|
-
[
|
|
109
|
-
bucketId,
|
|
110
|
-
pipelineId,
|
|
111
|
-
siloId,
|
|
112
|
-
prompt,
|
|
113
|
-
metadata,
|
|
114
|
-
aspectRatio,
|
|
115
|
-
model,
|
|
116
|
-
version,
|
|
117
|
-
resultId,
|
|
118
|
-
variables,
|
|
119
|
-
baseUrl,
|
|
120
|
-
original,
|
|
121
|
-
attachments,
|
|
122
|
-
],
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const [src, setSrc] = useState(rawSrc);
|
|
126
|
-
|
|
127
|
-
React.useEffect(() => {
|
|
128
|
-
const timer = setTimeout(() => {
|
|
129
|
-
setSrc(rawSrc);
|
|
130
|
-
}, 100);
|
|
131
|
-
return () => clearTimeout(timer);
|
|
132
|
-
}, [rawSrc]);
|
|
133
|
-
|
|
134
|
-
const alt = "Image produced by Slop Machine (slopmachine.dev)";
|
|
135
|
-
|
|
136
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
137
|
-
const [currentSrc, setCurrentSrc] = useState(src);
|
|
138
|
-
|
|
139
|
-
if (src !== currentSrc) {
|
|
140
|
-
setCurrentSrc(src);
|
|
141
|
-
setIsLoading(true);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
React.useEffect(() => {
|
|
145
|
-
if (!src) return;
|
|
146
|
-
|
|
147
|
-
fetch(src, { method: "HEAD" })
|
|
148
|
-
.then(async (res) => {
|
|
149
|
-
if (!res.ok) {
|
|
150
|
-
let errorMessage = res.statusText;
|
|
151
|
-
try {
|
|
152
|
-
const errRes = await fetch(src);
|
|
153
|
-
const errorText = await errRes.text();
|
|
154
|
-
const errorData = JSON.parse(errorText);
|
|
155
|
-
if (errorData.error) {
|
|
156
|
-
errorMessage = errorData.error;
|
|
157
|
-
}
|
|
158
|
-
} catch (e) {
|
|
159
|
-
// Failed to fetch or parse error details
|
|
160
|
-
}
|
|
161
|
-
console.error("SlopImage error:", res.status, errorMessage);
|
|
162
|
-
setIsLoading(false);
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
.catch((err) => {
|
|
166
|
-
console.error(`Error fetching image from ${src}:`, err);
|
|
167
|
-
setIsLoading(false);
|
|
168
|
-
});
|
|
169
|
-
}, [src]);
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<>
|
|
173
|
-
<style>{`
|
|
174
|
-
@keyframes slop-shimmer {
|
|
175
|
-
0% {
|
|
176
|
-
background-position: 200% 0;
|
|
177
|
-
}
|
|
178
|
-
100% {
|
|
179
|
-
background-position: -200% 0;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
@keyframes slop-spin {
|
|
183
|
-
from {
|
|
184
|
-
transform: rotate(0deg);
|
|
185
|
-
}
|
|
186
|
-
to {
|
|
187
|
-
transform: rotate(360deg);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
`}</style>
|
|
191
|
-
<div className={className} style={style}>
|
|
192
|
-
<div
|
|
193
|
-
className="slop-wrapper relative overflow-hidden w-full h-full"
|
|
194
|
-
style={{
|
|
195
|
-
aspectRatio: String(aspectRatio).replace(":", "/"),
|
|
196
|
-
position: "relative",
|
|
197
|
-
overflow: "hidden",
|
|
198
|
-
width: "100%",
|
|
199
|
-
height: "100%",
|
|
200
|
-
}}
|
|
201
|
-
>
|
|
202
|
-
{/* Loading UI */}
|
|
203
|
-
{loader ? (
|
|
204
|
-
<div
|
|
205
|
-
className={cn(
|
|
206
|
-
"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
|
|
207
|
-
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
208
|
-
)}
|
|
209
|
-
style={{
|
|
210
|
-
position: "absolute",
|
|
211
|
-
top: 0,
|
|
212
|
-
left: 0,
|
|
213
|
-
right: 0,
|
|
214
|
-
bottom: 0,
|
|
215
|
-
zIndex: 10,
|
|
216
|
-
display: "flex",
|
|
217
|
-
alignItems: "center",
|
|
218
|
-
justifyContent: "center",
|
|
219
|
-
opacity: isLoading ? 1 : 0,
|
|
220
|
-
pointerEvents: isLoading ? "auto" : "none",
|
|
221
|
-
transition: "opacity 300ms ease-in-out",
|
|
222
|
-
}}
|
|
223
|
-
>
|
|
224
|
-
{loader}
|
|
225
|
-
</div>
|
|
226
|
-
) : (
|
|
227
|
-
<>
|
|
228
|
-
{/* Loading overlay */}
|
|
229
|
-
<div
|
|
230
|
-
className={cn(
|
|
231
|
-
"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
|
|
232
|
-
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
233
|
-
)}
|
|
234
|
-
style={{
|
|
235
|
-
position: "absolute",
|
|
236
|
-
top: 0,
|
|
237
|
-
left: 0,
|
|
238
|
-
right: 0,
|
|
239
|
-
bottom: 0,
|
|
240
|
-
zIndex: 10,
|
|
241
|
-
display: "flex",
|
|
242
|
-
alignItems: "center",
|
|
243
|
-
justifyContent: "center",
|
|
244
|
-
backgroundColor: "var(--muted, #f3f4f6)",
|
|
245
|
-
opacity: isLoading ? 1 : 0,
|
|
246
|
-
pointerEvents: isLoading ? "auto" : "none",
|
|
247
|
-
transition: "opacity 300ms ease-in-out",
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
<div
|
|
251
|
-
className="flex flex-col items-center gap-2"
|
|
252
|
-
style={{
|
|
253
|
-
display: "flex",
|
|
254
|
-
flexDirection: "column",
|
|
255
|
-
alignItems: "center",
|
|
256
|
-
gap: "0.5rem",
|
|
257
|
-
}}
|
|
258
|
-
>
|
|
259
|
-
<svg
|
|
260
|
-
className="spinner size-6 text-muted-foreground"
|
|
261
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
262
|
-
fill="none"
|
|
263
|
-
viewBox="0 0 24 24"
|
|
264
|
-
style={{
|
|
265
|
-
width: "24px",
|
|
266
|
-
height: "24px",
|
|
267
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
268
|
-
animation: "slop-spin 1s linear infinite",
|
|
269
|
-
}}
|
|
270
|
-
>
|
|
271
|
-
<circle
|
|
272
|
-
className="opacity-25"
|
|
273
|
-
cx="12"
|
|
274
|
-
cy="12"
|
|
275
|
-
r="10"
|
|
276
|
-
stroke="currentColor"
|
|
277
|
-
strokeWidth="4"
|
|
278
|
-
style={{ opacity: 0.25 }}
|
|
279
|
-
/>
|
|
280
|
-
<path
|
|
281
|
-
className="opacity-75"
|
|
282
|
-
fill="currentColor"
|
|
283
|
-
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"
|
|
284
|
-
style={{ opacity: 0.75 }}
|
|
285
|
-
/>
|
|
286
|
-
</svg>
|
|
287
|
-
<span
|
|
288
|
-
className="text-xs text-muted-foreground"
|
|
289
|
-
style={{
|
|
290
|
-
fontSize: "0.75rem",
|
|
291
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
292
|
-
}}
|
|
293
|
-
>
|
|
294
|
-
Loading...
|
|
295
|
-
</span>
|
|
296
|
-
</div>
|
|
297
|
-
</div>
|
|
298
|
-
|
|
299
|
-
{/* Shimmer effect underneath */}
|
|
300
|
-
<div
|
|
301
|
-
className={cn(
|
|
302
|
-
"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
|
|
303
|
-
isLoading ? "opacity-100" : "opacity-0",
|
|
304
|
-
)}
|
|
305
|
-
style={{
|
|
306
|
-
position: "absolute",
|
|
307
|
-
top: 0,
|
|
308
|
-
left: 0,
|
|
309
|
-
right: 0,
|
|
310
|
-
bottom: 0,
|
|
311
|
-
background:
|
|
312
|
-
"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
|
|
313
|
-
backgroundSize: "200% 100%",
|
|
314
|
-
animation: "slop-shimmer 2s ease-in-out infinite",
|
|
315
|
-
opacity: isLoading ? 1 : 0,
|
|
316
|
-
transition: "opacity 300ms",
|
|
317
|
-
}}
|
|
318
|
-
/>
|
|
319
|
-
</>
|
|
320
|
-
)}
|
|
321
|
-
|
|
322
|
-
{/* Image */}
|
|
323
|
-
<img
|
|
324
|
-
key={src}
|
|
325
|
-
src={src}
|
|
326
|
-
alt={alt}
|
|
327
|
-
{...props}
|
|
328
|
-
onLoad={(e) => {
|
|
329
|
-
setIsLoading(false);
|
|
330
|
-
onLoad?.(e);
|
|
331
|
-
}}
|
|
332
|
-
onError={(e) => {
|
|
333
|
-
setIsLoading(false);
|
|
334
|
-
onError?.(e);
|
|
335
|
-
}}
|
|
336
|
-
className={cn(
|
|
337
|
-
"h-full w-full transition-opacity duration-500",
|
|
338
|
-
isLoading ? "opacity-0" : "opacity-100",
|
|
339
|
-
imageClassName,
|
|
340
|
-
)}
|
|
341
|
-
style={{
|
|
342
|
-
height: "100%",
|
|
343
|
-
width: "100%",
|
|
344
|
-
objectFit,
|
|
345
|
-
transition: "opacity 500ms",
|
|
346
|
-
opacity: isLoading ? 0 : 1,
|
|
347
|
-
}}
|
|
348
|
-
/>
|
|
349
|
-
</div>
|
|
350
|
-
</div>
|
|
351
|
-
</>
|
|
352
|
-
);
|
|
353
|
-
};
|
|
@@ -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.6.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";
|