@slopmachine/react 0.1.8 → 0.1.10

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