@slopmachine/react 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,29 +1,20 @@
1
1
  {
2
2
  "name": "@slopmachine/react",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "The official React SDK for Slop Machine",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": {
11
- "types": "./dist/index.d.mts",
12
- "default": "./dist/index.mjs"
13
- },
14
- "require": {
15
- "types": "./dist/index.d.ts",
16
- "default": "./dist/index.js"
17
- }
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
18
13
  }
19
14
  },
20
- "files": [
21
- "dist"
22
- ],
23
15
  "scripts": {
24
16
  "build": "tsup",
25
- "dev": "tsup --watch",
26
- "prepublishOnly": "npm run build"
17
+ "dev": "tsup --watch"
27
18
  },
28
19
  "repository": {
29
20
  "type": "git",
@@ -0,0 +1,231 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import { clsx, type ClassValue } from "clsx";
3
+ import { twMerge } from "tailwind-merge";
4
+
5
+ import { buildImageUrl, type SlopImageOptions } from "@slopmachine/core";
6
+
7
+ // Utility for Tailwind classes
8
+ function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }
11
+
12
+ interface SlopImageProps
13
+ extends
14
+ Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">,
15
+ SlopImageOptions {}
16
+
17
+ export const SlopImage: React.FC<SlopImageProps> = ({
18
+ bucketId,
19
+ className,
20
+ aspectRatio = "1:1",
21
+ version,
22
+ resultId,
23
+ model,
24
+ variables = {},
25
+ baseUrl,
26
+ ...props
27
+ }) => {
28
+ // Construct API URL
29
+ const rawSrc = useMemo(
30
+ () =>
31
+ buildImageUrl({
32
+ bucketId,
33
+ aspectRatio,
34
+ version,
35
+ resultId,
36
+ variables,
37
+ baseUrl,
38
+ model,
39
+ }),
40
+ [bucketId, aspectRatio, version, resultId, variables, baseUrl, model],
41
+ );
42
+
43
+ const [src, setSrc] = useState(rawSrc);
44
+
45
+ React.useEffect(() => {
46
+ const timer = setTimeout(() => {
47
+ setSrc(rawSrc);
48
+ }, 100);
49
+ return () => clearTimeout(timer);
50
+ }, [rawSrc]);
51
+
52
+ const alt = "Generated image";
53
+
54
+ const [isLoading, setIsLoading] = useState(true);
55
+ const [currentSrc, setCurrentSrc] = useState(src);
56
+
57
+ if (src !== currentSrc) {
58
+ setCurrentSrc(src);
59
+ setIsLoading(true);
60
+ }
61
+
62
+ React.useEffect(() => {
63
+ if (!src) return;
64
+
65
+ fetch(src, { method: "HEAD" })
66
+ .then(async (res) => {
67
+ if (!res.ok) {
68
+ // console.error(
69
+ // `Failed to load image from ${src}. Status: ${res.status} ${res.statusText}`,
70
+ // );
71
+ try {
72
+ const errRes = await fetch(src);
73
+ const errJson = await errRes.json();
74
+ console.error("SlopImage error:", res.status, errJson.error);
75
+ } catch (e) {
76
+ // Failed to fetch or parse error details
77
+ }
78
+ setIsLoading(false);
79
+ }
80
+ })
81
+ .catch((err) => {
82
+ console.error(`Error fetching image from ${src}:`, err);
83
+ setIsLoading(false);
84
+ });
85
+ }, [src]);
86
+
87
+ return (
88
+ <>
89
+ <style>{`
90
+ @keyframes slop-shimmer {
91
+ 0% {
92
+ background-position: 200% 0;
93
+ }
94
+ 100% {
95
+ background-position: -200% 0;
96
+ }
97
+ }
98
+ @keyframes slop-spin {
99
+ from {
100
+ transform: rotate(0deg);
101
+ }
102
+ to {
103
+ transform: rotate(360deg);
104
+ }
105
+ }
106
+ `}</style>
107
+ <div
108
+ className={cn("relative overflow-hidden", className)}
109
+ style={{
110
+ aspectRatio: String(aspectRatio).replace(":", "/"),
111
+ backgroundColor: "var(--muted, #f3f4f6)",
112
+ position: "relative",
113
+ overflow: "hidden",
114
+ }}
115
+ >
116
+ {/* Loading overlay */}
117
+ <div
118
+ className={cn(
119
+ "absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
120
+ isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
121
+ )}
122
+ style={{
123
+ position: "absolute",
124
+ top: 0,
125
+ left: 0,
126
+ right: 0,
127
+ bottom: 0,
128
+ zIndex: 10,
129
+ display: "flex",
130
+ alignItems: "center",
131
+ justifyContent: "center",
132
+ backgroundColor: "var(--muted, #f3f4f6)",
133
+ opacity: isLoading ? 1 : 0,
134
+ pointerEvents: isLoading ? "auto" : "none",
135
+ transition: "opacity 300ms ease-in-out",
136
+ }}
137
+ >
138
+ <div
139
+ className="flex flex-col items-center gap-2"
140
+ style={{
141
+ display: "flex",
142
+ flexDirection: "column",
143
+ alignItems: "center",
144
+ gap: "0.5rem",
145
+ }}
146
+ >
147
+ <svg
148
+ className="spinner size-6 text-muted-foreground"
149
+ xmlns="http://www.w3.org/2000/svg"
150
+ fill="none"
151
+ viewBox="0 0 24 24"
152
+ style={{
153
+ width: "24px",
154
+ height: "24px",
155
+ color: "var(--muted-foreground, #6b7280)",
156
+ animation: "slop-spin 1s linear infinite",
157
+ }}
158
+ >
159
+ <circle
160
+ className="opacity-25"
161
+ cx="12"
162
+ cy="12"
163
+ r="10"
164
+ stroke="currentColor"
165
+ strokeWidth="4"
166
+ style={{ opacity: 0.25 }}
167
+ />
168
+ <path
169
+ className="opacity-75"
170
+ fill="currentColor"
171
+ 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"
172
+ style={{ opacity: 0.75 }}
173
+ />
174
+ </svg>
175
+ <span
176
+ className="text-xs text-muted-foreground"
177
+ style={{
178
+ fontSize: "0.75rem",
179
+ color: "var(--muted-foreground, #6b7280)",
180
+ }}
181
+ >
182
+ Loading...
183
+ </span>
184
+ </div>
185
+ </div>
186
+
187
+ {/* Shimmer effect underneath */}
188
+ <div
189
+ className={cn(
190
+ "shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
191
+ isLoading ? "opacity-100" : "opacity-0",
192
+ )}
193
+ style={{
194
+ position: "absolute",
195
+ top: 0,
196
+ left: 0,
197
+ right: 0,
198
+ bottom: 0,
199
+ background:
200
+ "linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
201
+ backgroundSize: "200% 100%",
202
+ animation: "slop-shimmer 2s ease-in-out infinite",
203
+ opacity: isLoading ? 1 : 0,
204
+ transition: "opacity 300ms",
205
+ }}
206
+ />
207
+
208
+ {/* Image */}
209
+ <img
210
+ key={src}
211
+ src={src}
212
+ alt={alt}
213
+ onLoad={() => setIsLoading(false)}
214
+ onError={() => setIsLoading(false)}
215
+ className={cn(
216
+ "h-full w-full object-cover transition-opacity duration-500",
217
+ isLoading ? "opacity-0" : "opacity-100",
218
+ )}
219
+ style={{
220
+ height: "100%",
221
+ width: "100%",
222
+ objectFit: "cover",
223
+ transition: "opacity 500ms",
224
+ opacity: isLoading ? 0 : 1,
225
+ }}
226
+ {...props}
227
+ />
228
+ </div>
229
+ </>
230
+ );
231
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { SlopImage } from './components/SlopImage';
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
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
+ "baseUrl": "."
25
+ },
26
+ "include": ["src"],
27
+ "exclude": ["node_modules", "dist"]
28
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ external: ['react', 'react-dom'],
9
+ });