@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slopmachine/react",
3
- "version": "0.7.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
- "types": "./dist/index.d.ts",
13
- "import": "./dist/index.mjs",
14
- "require": "./dist/index.js",
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.7.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
- };