@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slopmachine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"llms": "https://docs.slopmachine.dev/llms.txt",
|
|
5
5
|
"llmsFull": "https://docs.slopmachine.dev/llms-full.txt",
|
|
6
6
|
"description": "The official React SDK for Slop Machine",
|
|
@@ -9,15 +9,21 @@
|
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"default": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
15
20
|
"default": "./dist/index.js"
|
|
16
21
|
}
|
|
17
22
|
},
|
|
18
23
|
"scripts": {
|
|
19
24
|
"build": "tsup",
|
|
20
|
-
"dev": "tsup --watch"
|
|
25
|
+
"dev": "tsup --watch",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
21
27
|
},
|
|
22
28
|
"repository": {
|
|
23
29
|
"type": "git",
|
|
@@ -44,11 +50,16 @@
|
|
|
44
50
|
"vite": "^7.3.1"
|
|
45
51
|
},
|
|
46
52
|
"dependencies": {
|
|
47
|
-
"@slopmachine/core": "^0.
|
|
53
|
+
"@slopmachine/core": "^0.8.1",
|
|
48
54
|
"clsx": "^2.1.1",
|
|
49
55
|
"tailwind-merge": "^3.4.0"
|
|
50
56
|
},
|
|
51
57
|
"peerDependencies": {
|
|
52
58
|
"react": ">=18"
|
|
53
|
-
}
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist",
|
|
62
|
+
"CHANGELOG.md"
|
|
63
|
+
],
|
|
64
|
+
"sideEffects": false
|
|
54
65
|
}
|
package/Agents.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# React SDK Conventions
|
|
2
|
-
|
|
3
|
-
This document outlines the coding conventions and patterns used in the `@slopmachine/react` SDK. Future agents should adhere to these guidelines when making modifications or adding new components.
|
|
4
|
-
|
|
5
|
-
> **Important:** Review the repository-wide guidelines in [AGENTS.md](../../AGENTS.md) and check [GOTCHAS.md](../../GOTCHAS.md) for known issues, pitfalls, and workarounds before starting tasks. Maintain `GOTCHAS.md` whenever resolving new pitfalls.
|
|
6
|
-
|
|
7
|
-
## Dependencies and Styling
|
|
8
|
-
|
|
9
|
-
- **Class Merging:** Use `clsx` and `tailwind-merge` to construct and merge Tailwind CSS classes. A utility function `cn(...inputs: ClassValue[])` is typically used for this purpose.
|
|
10
|
-
- **Styling Approach:** Components use Tailwind CSS classes for styling. However, to ensure components remain robust even if the consumer's environment doesn't perfectly process all Tailwind classes, always include equivalent inline `style` properties as a fallback.
|
|
11
|
-
- **CSS Variables:** Use standard CSS variables for theme-related colors (e.g., `var(--muted, #f3f4f6)`).
|
|
12
|
-
- **Custom Animations/Styles:** If complex animations or specific scoped styles are needed that go beyond Tailwind utility classes, inject them using a scoped `<style>` block within the component.
|
|
13
|
-
- **Core Logic:** Import shared logic, utilities, and base types from `@slopmachine/core`.
|
|
14
|
-
|
|
15
|
-
## Component Structure
|
|
16
|
-
|
|
17
|
-
- **Functional Components:** Define components as Functional Components (e.g., `React.FC<Props>`).
|
|
18
|
-
- **Prop Forwarding:** Spread the remaining props (`...props`) onto the primary underlying DOM element to allow consumers to pass standard HTML attributes (like `aria-` attributes, data attributes, etc.).
|
|
19
|
-
|
|
20
|
-
## Prop Typing
|
|
21
|
-
|
|
22
|
-
- **Interfaces:** Define an explicit `Props` interface for each component.
|
|
23
|
-
- **Extending Standard Attributes:** Extend the appropriate React HTML attributes interface (e.g., `React.ImgHTMLAttributes<HTMLImageElement>`).
|
|
24
|
-
- **Overriding Attributes:** Use `Omit` to exclude standard attributes that are managed internally by the component or replaced by custom SDK options (e.g., `Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">`).
|
|
25
|
-
- **Shared Types:** Extend or incorporate types from `@slopmachine/core` (like `SlopImageOptions`).
|
|
26
|
-
|
|
27
|
-
## IntelliSense and Documentation
|
|
28
|
-
|
|
29
|
-
- **JSDoc Comments:** Always include descriptive JSDoc comments for components and their props to provide rich IntelliSense in IDEs.
|
|
30
|
-
- **Component Documentation:** Component JSDoc blocks should describe what the component does and include a clear `@example` block demonstrating usage, and optionally a `@version` tag if applicable.
|
|
31
|
-
- **Prop Documentation:** Every custom prop in a component's interface should have a JSDoc comment explaining its purpose, default behavior, and any edge cases. This is crucial for developer experience when consuming the SDK.
|
|
32
|
-
|
|
33
|
-
## Exporting Patterns
|
|
34
|
-
|
|
35
|
-
- **Named Exports:** Use named exports for components and interfaces (e.g., `export const ComponentName = ...`). Avoid default exports.
|
|
36
|
-
- **Entry Point:** Ensure components and their prop types are exported from the package's main entry point if they are meant for public consumption.
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type { SlopMachineError } from "@slopmachine/core";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Content to render when generation or loading fails. Either a node, or a
|
|
6
|
-
* function that receives the error and returns a node.
|
|
7
|
-
*/
|
|
8
|
-
export type SlopErrorFallback =
|
|
9
|
-
React.ReactNode | ((error: SlopMachineError) => React.ReactNode);
|
|
10
|
-
|
|
11
|
-
interface ErrorOverlayProps {
|
|
12
|
-
error: SlopMachineError;
|
|
13
|
-
errorFallback?: SlopErrorFallback;
|
|
14
|
-
/**
|
|
15
|
-
* Message shown by the default error UI, e.g. "Failed to load image".
|
|
16
|
-
*/
|
|
17
|
-
label: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const overlayStyle: React.CSSProperties = {
|
|
21
|
-
position: "absolute",
|
|
22
|
-
top: 0,
|
|
23
|
-
left: 0,
|
|
24
|
-
right: 0,
|
|
25
|
-
bottom: 0,
|
|
26
|
-
zIndex: 10,
|
|
27
|
-
display: "flex",
|
|
28
|
-
alignItems: "center",
|
|
29
|
-
justifyContent: "center",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Internal overlay shown over the media slot when generation or loading fails.
|
|
34
|
-
* Renders the consumer's `errorFallback` if provided, otherwise a default message.
|
|
35
|
-
*/
|
|
36
|
-
export const ErrorOverlay: React.FC<ErrorOverlayProps> = ({
|
|
37
|
-
error,
|
|
38
|
-
errorFallback,
|
|
39
|
-
label,
|
|
40
|
-
}) => {
|
|
41
|
-
if (errorFallback !== undefined) {
|
|
42
|
-
return (
|
|
43
|
-
<div
|
|
44
|
-
className="absolute inset-0 z-10 flex items-center justify-center"
|
|
45
|
-
style={overlayStyle}
|
|
46
|
-
>
|
|
47
|
-
{typeof errorFallback === "function"
|
|
48
|
-
? errorFallback(error)
|
|
49
|
-
: errorFallback}
|
|
50
|
-
</div>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<div
|
|
56
|
-
className="absolute inset-0 z-10 flex items-center justify-center bg-muted"
|
|
57
|
-
style={{ ...overlayStyle, backgroundColor: "var(--muted, #f3f4f6)" }}
|
|
58
|
-
title={error.message}
|
|
59
|
-
>
|
|
60
|
-
<div
|
|
61
|
-
className="flex flex-col items-center gap-2 text-muted-foreground"
|
|
62
|
-
style={{
|
|
63
|
-
display: "flex",
|
|
64
|
-
flexDirection: "column",
|
|
65
|
-
alignItems: "center",
|
|
66
|
-
gap: "0.5rem",
|
|
67
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
68
|
-
}}
|
|
69
|
-
>
|
|
70
|
-
<svg
|
|
71
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
-
viewBox="0 0 24 24"
|
|
73
|
-
fill="none"
|
|
74
|
-
stroke="currentColor"
|
|
75
|
-
strokeWidth="2"
|
|
76
|
-
strokeLinecap="round"
|
|
77
|
-
strokeLinejoin="round"
|
|
78
|
-
aria-hidden="true"
|
|
79
|
-
style={{ width: "24px", height: "24px" }}
|
|
80
|
-
>
|
|
81
|
-
<circle cx="12" cy="12" r="10" />
|
|
82
|
-
<line x1="12" y1="8" x2="12" y2="12" />
|
|
83
|
-
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
84
|
-
</svg>
|
|
85
|
-
<span className="text-xs" style={{ fontSize: "0.75rem" }}>
|
|
86
|
-
{label}
|
|
87
|
-
</span>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
};
|
|
@@ -1,376 +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
|
-
buildImageUrl,
|
|
7
|
-
createRenderUrlMonitor,
|
|
8
|
-
type SlopMachineError,
|
|
9
|
-
type SlopImageOptions,
|
|
10
|
-
type ImageAspectRatio,
|
|
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 SlopImageProps
|
|
20
|
-
extends
|
|
21
|
-
Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">,
|
|
22
|
-
Omit<SlopImageOptions, "aspectRatio"> {
|
|
23
|
-
/**
|
|
24
|
-
* The aspect ratio of the generated image. Defaults to "1:1".
|
|
25
|
-
*/
|
|
26
|
-
aspectRatio?: ImageAspectRatio;
|
|
27
|
-
/**
|
|
28
|
-
* Custom React node to display while the image 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 image 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 image.
|
|
36
|
-
*/
|
|
37
|
-
errorFallback?: SlopErrorFallback;
|
|
38
|
-
/**
|
|
39
|
-
* Called once per URL when the image 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
|
-
* How the image should be resized to fit its container. Defaults to "cover".
|
|
45
|
-
*/
|
|
46
|
-
objectFit?: React.CSSProperties["objectFit"];
|
|
47
|
-
/**
|
|
48
|
-
* Additional CSS classes to apply directly to the `<img />` element.
|
|
49
|
-
*/
|
|
50
|
-
imageClassName?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Renders an image generated by Slop Machine.
|
|
55
|
-
*
|
|
56
|
-
* This component automatically constructs the correct URL for the Slop Machine API,
|
|
57
|
-
* displays a loading skeleton or custom loader while fetching, and handles errors.
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```tsx
|
|
61
|
-
* // Using a Bucket
|
|
62
|
-
* <SlopImage
|
|
63
|
-
* bucketId="my-bucket"
|
|
64
|
-
* resultId="some-result"
|
|
65
|
-
* aspectRatio="16:9"
|
|
66
|
-
* className="rounded-lg"
|
|
67
|
-
* objectFit="cover"
|
|
68
|
-
* />
|
|
69
|
-
*
|
|
70
|
-
* // Using a Pipeline with runtime prompt
|
|
71
|
-
* <SlopImage
|
|
72
|
-
* pipelineId="pipe_live_abc123"
|
|
73
|
-
* prompt="1960s retro robot eating pizza on Mars"
|
|
74
|
-
* aspectRatio="16:9"
|
|
75
|
-
* metadata={{ userId: "123" }}
|
|
76
|
-
* />
|
|
77
|
-
* ```
|
|
78
|
-
*
|
|
79
|
-
* @version 0.7.0
|
|
80
|
-
*/
|
|
81
|
-
export const SlopImage: React.FC<SlopImageProps> = ({
|
|
82
|
-
bucketId,
|
|
83
|
-
pipelineId,
|
|
84
|
-
siloId,
|
|
85
|
-
prompt,
|
|
86
|
-
metadata,
|
|
87
|
-
className,
|
|
88
|
-
aspectRatio = "1:1",
|
|
89
|
-
model,
|
|
90
|
-
version,
|
|
91
|
-
resultId,
|
|
92
|
-
variables = {},
|
|
93
|
-
baseUrl,
|
|
94
|
-
original,
|
|
95
|
-
attachments,
|
|
96
|
-
loader,
|
|
97
|
-
errorFallback,
|
|
98
|
-
onGenerationError,
|
|
99
|
-
objectFit = "cover",
|
|
100
|
-
imageClassName,
|
|
101
|
-
style,
|
|
102
|
-
onLoad,
|
|
103
|
-
onError,
|
|
104
|
-
...props
|
|
105
|
-
}) => {
|
|
106
|
-
// Construct API URL
|
|
107
|
-
const rawSrc = useMemo(
|
|
108
|
-
() =>
|
|
109
|
-
buildImageUrl({
|
|
110
|
-
bucketId,
|
|
111
|
-
pipelineId,
|
|
112
|
-
siloId,
|
|
113
|
-
prompt,
|
|
114
|
-
metadata,
|
|
115
|
-
aspectRatio,
|
|
116
|
-
model,
|
|
117
|
-
version,
|
|
118
|
-
resultId,
|
|
119
|
-
variables,
|
|
120
|
-
baseUrl,
|
|
121
|
-
original,
|
|
122
|
-
attachments,
|
|
123
|
-
}),
|
|
124
|
-
[
|
|
125
|
-
bucketId,
|
|
126
|
-
pipelineId,
|
|
127
|
-
siloId,
|
|
128
|
-
prompt,
|
|
129
|
-
metadata,
|
|
130
|
-
aspectRatio,
|
|
131
|
-
model,
|
|
132
|
-
version,
|
|
133
|
-
resultId,
|
|
134
|
-
variables,
|
|
135
|
-
baseUrl,
|
|
136
|
-
original,
|
|
137
|
-
attachments,
|
|
138
|
-
],
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const [src, setSrc] = useState(rawSrc);
|
|
142
|
-
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
const timer = setTimeout(() => {
|
|
145
|
-
setSrc(rawSrc);
|
|
146
|
-
}, 100);
|
|
147
|
-
return () => clearTimeout(timer);
|
|
148
|
-
}, [rawSrc]);
|
|
149
|
-
|
|
150
|
-
const alt = "Image produced by Slop Machine (slopmachine.dev)";
|
|
151
|
-
|
|
152
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
153
|
-
const [error, setError] = useState<SlopMachineError | null>(null);
|
|
154
|
-
const [currentSrc, setCurrentSrc] = useState(src);
|
|
155
|
-
if (src !== currentSrc) {
|
|
156
|
-
setCurrentSrc(src);
|
|
157
|
-
setIsLoading(true);
|
|
158
|
-
setError(null);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Keep the latest callback without re-running effects when it changes identity
|
|
162
|
-
const onGenerationErrorRef = useRef(onGenerationError);
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
onGenerationErrorRef.current = onGenerationError;
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Shares one URL check between the HEAD probe and the media element's error event
|
|
168
|
-
const monitor = useMemo(
|
|
169
|
-
() =>
|
|
170
|
-
createRenderUrlMonitor((err) => {
|
|
171
|
-
console.error("SlopImage error:", err.status ?? "", err.message);
|
|
172
|
-
setIsLoading(false);
|
|
173
|
-
setError(err);
|
|
174
|
-
onGenerationErrorRef.current?.(err);
|
|
175
|
-
}),
|
|
176
|
-
[],
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
if (!src) return;
|
|
181
|
-
monitor.watch(src);
|
|
182
|
-
return () => monitor.stop();
|
|
183
|
-
}, [src, monitor]);
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<>
|
|
187
|
-
<style>{`
|
|
188
|
-
@keyframes slop-shimmer {
|
|
189
|
-
0% {
|
|
190
|
-
background-position: 200% 0;
|
|
191
|
-
}
|
|
192
|
-
100% {
|
|
193
|
-
background-position: -200% 0;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
@keyframes slop-spin {
|
|
197
|
-
from {
|
|
198
|
-
transform: rotate(0deg);
|
|
199
|
-
}
|
|
200
|
-
to {
|
|
201
|
-
transform: rotate(360deg);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
`}</style>
|
|
205
|
-
<div className={className} style={style}>
|
|
206
|
-
<div
|
|
207
|
-
className="slop-wrapper relative overflow-hidden w-full h-full"
|
|
208
|
-
style={{
|
|
209
|
-
aspectRatio: String(aspectRatio).replace(":", "/"),
|
|
210
|
-
position: "relative",
|
|
211
|
-
overflow: "hidden",
|
|
212
|
-
width: "100%",
|
|
213
|
-
height: "100%",
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
216
|
-
{/* Loading UI */}
|
|
217
|
-
{loader ? (
|
|
218
|
-
<div
|
|
219
|
-
className={cn(
|
|
220
|
-
"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
|
|
221
|
-
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
222
|
-
)}
|
|
223
|
-
style={{
|
|
224
|
-
position: "absolute",
|
|
225
|
-
top: 0,
|
|
226
|
-
left: 0,
|
|
227
|
-
right: 0,
|
|
228
|
-
bottom: 0,
|
|
229
|
-
zIndex: 10,
|
|
230
|
-
display: "flex",
|
|
231
|
-
alignItems: "center",
|
|
232
|
-
justifyContent: "center",
|
|
233
|
-
opacity: isLoading ? 1 : 0,
|
|
234
|
-
pointerEvents: isLoading ? "auto" : "none",
|
|
235
|
-
transition: "opacity 300ms ease-in-out",
|
|
236
|
-
}}
|
|
237
|
-
>
|
|
238
|
-
{loader}
|
|
239
|
-
</div>
|
|
240
|
-
) : (
|
|
241
|
-
<>
|
|
242
|
-
{/* Loading overlay */}
|
|
243
|
-
<div
|
|
244
|
-
className={cn(
|
|
245
|
-
"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
|
|
246
|
-
isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
247
|
-
)}
|
|
248
|
-
style={{
|
|
249
|
-
position: "absolute",
|
|
250
|
-
top: 0,
|
|
251
|
-
left: 0,
|
|
252
|
-
right: 0,
|
|
253
|
-
bottom: 0,
|
|
254
|
-
zIndex: 10,
|
|
255
|
-
display: "flex",
|
|
256
|
-
alignItems: "center",
|
|
257
|
-
justifyContent: "center",
|
|
258
|
-
backgroundColor: "var(--muted, #f3f4f6)",
|
|
259
|
-
opacity: isLoading ? 1 : 0,
|
|
260
|
-
pointerEvents: isLoading ? "auto" : "none",
|
|
261
|
-
transition: "opacity 300ms ease-in-out",
|
|
262
|
-
}}
|
|
263
|
-
>
|
|
264
|
-
<div
|
|
265
|
-
className="flex flex-col items-center gap-2"
|
|
266
|
-
style={{
|
|
267
|
-
display: "flex",
|
|
268
|
-
flexDirection: "column",
|
|
269
|
-
alignItems: "center",
|
|
270
|
-
gap: "0.5rem",
|
|
271
|
-
}}
|
|
272
|
-
>
|
|
273
|
-
<svg
|
|
274
|
-
className="spinner size-6 text-muted-foreground"
|
|
275
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
276
|
-
fill="none"
|
|
277
|
-
viewBox="0 0 24 24"
|
|
278
|
-
style={{
|
|
279
|
-
width: "24px",
|
|
280
|
-
height: "24px",
|
|
281
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
282
|
-
animation: "slop-spin 1s linear infinite",
|
|
283
|
-
}}
|
|
284
|
-
>
|
|
285
|
-
<circle
|
|
286
|
-
className="opacity-25"
|
|
287
|
-
cx="12"
|
|
288
|
-
cy="12"
|
|
289
|
-
r="10"
|
|
290
|
-
stroke="currentColor"
|
|
291
|
-
strokeWidth="4"
|
|
292
|
-
style={{ opacity: 0.25 }}
|
|
293
|
-
/>
|
|
294
|
-
<path
|
|
295
|
-
className="opacity-75"
|
|
296
|
-
fill="currentColor"
|
|
297
|
-
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"
|
|
298
|
-
style={{ opacity: 0.75 }}
|
|
299
|
-
/>
|
|
300
|
-
</svg>
|
|
301
|
-
<span
|
|
302
|
-
className="text-xs text-muted-foreground"
|
|
303
|
-
style={{
|
|
304
|
-
fontSize: "0.75rem",
|
|
305
|
-
color: "var(--muted-foreground, #6b7280)",
|
|
306
|
-
}}
|
|
307
|
-
>
|
|
308
|
-
Loading...
|
|
309
|
-
</span>
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
|
|
313
|
-
{/* Shimmer effect underneath */}
|
|
314
|
-
<div
|
|
315
|
-
className={cn(
|
|
316
|
-
"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
|
|
317
|
-
isLoading ? "opacity-100" : "opacity-0",
|
|
318
|
-
)}
|
|
319
|
-
style={{
|
|
320
|
-
position: "absolute",
|
|
321
|
-
top: 0,
|
|
322
|
-
left: 0,
|
|
323
|
-
right: 0,
|
|
324
|
-
bottom: 0,
|
|
325
|
-
background:
|
|
326
|
-
"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
|
|
327
|
-
backgroundSize: "200% 100%",
|
|
328
|
-
animation: "slop-shimmer 2s ease-in-out infinite",
|
|
329
|
-
opacity: isLoading ? 1 : 0,
|
|
330
|
-
transition: "opacity 300ms",
|
|
331
|
-
}}
|
|
332
|
-
/>
|
|
333
|
-
</>
|
|
334
|
-
)}
|
|
335
|
-
|
|
336
|
-
{error && (
|
|
337
|
-
<ErrorOverlay
|
|
338
|
-
error={error}
|
|
339
|
-
errorFallback={errorFallback}
|
|
340
|
-
label="Failed to load image"
|
|
341
|
-
/>
|
|
342
|
-
)}
|
|
343
|
-
|
|
344
|
-
{/* Image */}
|
|
345
|
-
<img
|
|
346
|
-
key={src}
|
|
347
|
-
src={src}
|
|
348
|
-
alt={alt}
|
|
349
|
-
{...props}
|
|
350
|
-
onLoad={(e) => {
|
|
351
|
-
setIsLoading(false);
|
|
352
|
-
onLoad?.(e);
|
|
353
|
-
}}
|
|
354
|
-
onError={(e) => {
|
|
355
|
-
monitor.mediaFailed(src, "Failed to load image");
|
|
356
|
-
onError?.(e);
|
|
357
|
-
}}
|
|
358
|
-
aria-hidden={error ? true : props["aria-hidden"]}
|
|
359
|
-
className={cn(
|
|
360
|
-
"h-full w-full transition-opacity duration-500",
|
|
361
|
-
isLoading || error ? "opacity-0" : "opacity-100",
|
|
362
|
-
imageClassName,
|
|
363
|
-
)}
|
|
364
|
-
style={{
|
|
365
|
-
height: "100%",
|
|
366
|
-
width: "100%",
|
|
367
|
-
objectFit,
|
|
368
|
-
transition: "opacity 500ms",
|
|
369
|
-
opacity: isLoading || error ? 0 : 1,
|
|
370
|
-
}}
|
|
371
|
-
/>
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
374
|
-
</>
|
|
375
|
-
);
|
|
376
|
-
};
|