@slopmachine/react 0.1.10 → 0.1.12

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/Agents.md ADDED
@@ -0,0 +1,34 @@
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
+ ## Dependencies and Styling
6
+
7
+ - **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.
8
+ - **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.
9
+ - **CSS Variables:** Use standard CSS variables for theme-related colors (e.g., `var(--muted, #f3f4f6)`).
10
+ - **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.
11
+ - **Core Logic:** Import shared logic, utilities, and base types from `@slopmachine/core`.
12
+
13
+ ## Component Structure
14
+
15
+ - **Functional Components:** Define components as Functional Components (e.g., `React.FC<Props>`).
16
+ - **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.).
17
+
18
+ ## Prop Typing
19
+
20
+ - **Interfaces:** Define an explicit `Props` interface for each component.
21
+ - **Extending Standard Attributes:** Extend the appropriate React HTML attributes interface (e.g., `React.ImgHTMLAttributes<HTMLImageElement>`).
22
+ - **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">`).
23
+ - **Shared Types:** Extend or incorporate types from `@slopmachine/core` (like `SlopImageOptions`).
24
+
25
+ ## IntelliSense and Documentation
26
+
27
+ - **JSDoc Comments:** Always include descriptive JSDoc comments for components and their props to provide rich IntelliSense in IDEs.
28
+ - **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.
29
+ - **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.
30
+
31
+ ## Exporting Patterns
32
+
33
+ - **Named Exports:** Use named exports for components and interfaces (e.g., `export const ComponentName = ...`). Avoid default exports.
34
+ - **Entry Point:** Ensure components and their prop types are exported from the package's main entry point if they are meant for public consumption.
package/README.md CHANGED
@@ -14,7 +14,7 @@ pnpm add @slopmachine/react
14
14
 
15
15
  ## Demo
16
16
 
17
- https://slopmachine-dev.github.io/slopmachine-sdk/
17
+ https://slopmachine-dev.github.io/slopmachine-sdk/demo-react/
18
18
 
19
19
  ## Usage
20
20
 
package/dist/index.d.mts CHANGED
@@ -1,9 +1,68 @@
1
1
  import React from 'react';
2
- import { SlopImageOptions } from '@slopmachine/core';
2
+ import { SlopImageOptions, ImageAspectRatio, SlopVideoOptions, VideoAspectRatio } from '@slopmachine/core';
3
+ export { ImageAspectRatio, SlopImageOptions, SlopVideoOptions, VideoAspectRatio } from '@slopmachine/core';
3
4
 
4
- interface SlopImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">, SlopImageOptions {
5
+ interface SlopImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">, Omit<SlopImageOptions, "aspectRatio"> {
6
+ /**
7
+ * The aspect ratio of the generated image. Defaults to "1:1".
8
+ */
9
+ aspectRatio?: ImageAspectRatio;
10
+ /**
11
+ * Custom React node to display while the image is loading.
12
+ * If not provided, a default spinner and shimmer effect will be shown.
13
+ */
5
14
  loader?: React.ReactNode;
6
15
  }
16
+ /**
17
+ * Renders an image generated by Slop Machine.
18
+ *
19
+ * This component automatically constructs the correct URL for the Slop Machine API,
20
+ * displays a loading skeleton or custom loader while fetching, and handles errors.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <SlopImage
25
+ * bucketId="my-bucket"
26
+ * resultId="some-result"
27
+ * aspectRatio="16:9"
28
+ * className="rounded-lg"
29
+ * />
30
+ * ```
31
+ *
32
+ * @version 0.1.12
33
+ */
7
34
  declare const SlopImage: React.FC<SlopImageProps>;
8
35
 
9
- export { SlopImage };
36
+ interface SlopVideoProps extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "src">, Omit<SlopVideoOptions, "aspectRatio"> {
37
+ /**
38
+ * The aspect ratio of the generated video. Defaults to "16:9".
39
+ */
40
+ aspectRatio?: VideoAspectRatio;
41
+ /**
42
+ * Custom React node to display while the video is loading.
43
+ * If not provided, a default spinner and shimmer effect will be shown.
44
+ */
45
+ loader?: React.ReactNode;
46
+ }
47
+ /**
48
+ * Renders a video generated by Slop Machine.
49
+ *
50
+ * This component automatically constructs the correct URL for the Slop Machine API,
51
+ * displays a loading skeleton or custom loader while fetching, and handles errors.
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * <SlopVideo
56
+ * bucketId="my-bucket"
57
+ * resultId="some-result"
58
+ * aspectRatio="16:9"
59
+ * className="rounded-lg"
60
+ * autoPlay
61
+ * loop
62
+ * muted
63
+ * />
64
+ * ```
65
+ */
66
+ declare const SlopVideo: React.FC<SlopVideoProps>;
67
+
68
+ export { SlopImage, type SlopImageProps, SlopVideo, type SlopVideoProps };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,68 @@
1
1
  import React from 'react';
2
- import { SlopImageOptions } from '@slopmachine/core';
2
+ import { SlopImageOptions, ImageAspectRatio, SlopVideoOptions, VideoAspectRatio } from '@slopmachine/core';
3
+ export { ImageAspectRatio, SlopImageOptions, SlopVideoOptions, VideoAspectRatio } from '@slopmachine/core';
3
4
 
4
- interface SlopImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">, SlopImageOptions {
5
+ interface SlopImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">, Omit<SlopImageOptions, "aspectRatio"> {
6
+ /**
7
+ * The aspect ratio of the generated image. Defaults to "1:1".
8
+ */
9
+ aspectRatio?: ImageAspectRatio;
10
+ /**
11
+ * Custom React node to display while the image is loading.
12
+ * If not provided, a default spinner and shimmer effect will be shown.
13
+ */
5
14
  loader?: React.ReactNode;
6
15
  }
16
+ /**
17
+ * Renders an image generated by Slop Machine.
18
+ *
19
+ * This component automatically constructs the correct URL for the Slop Machine API,
20
+ * displays a loading skeleton or custom loader while fetching, and handles errors.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <SlopImage
25
+ * bucketId="my-bucket"
26
+ * resultId="some-result"
27
+ * aspectRatio="16:9"
28
+ * className="rounded-lg"
29
+ * />
30
+ * ```
31
+ *
32
+ * @version 0.1.12
33
+ */
7
34
  declare const SlopImage: React.FC<SlopImageProps>;
8
35
 
9
- export { SlopImage };
36
+ interface SlopVideoProps extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "src">, Omit<SlopVideoOptions, "aspectRatio"> {
37
+ /**
38
+ * The aspect ratio of the generated video. Defaults to "16:9".
39
+ */
40
+ aspectRatio?: VideoAspectRatio;
41
+ /**
42
+ * Custom React node to display while the video is loading.
43
+ * If not provided, a default spinner and shimmer effect will be shown.
44
+ */
45
+ loader?: React.ReactNode;
46
+ }
47
+ /**
48
+ * Renders a video generated by Slop Machine.
49
+ *
50
+ * This component automatically constructs the correct URL for the Slop Machine API,
51
+ * displays a loading skeleton or custom loader while fetching, and handles errors.
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * <SlopVideo
56
+ * bucketId="my-bucket"
57
+ * resultId="some-result"
58
+ * aspectRatio="16:9"
59
+ * className="rounded-lg"
60
+ * autoPlay
61
+ * loop
62
+ * muted
63
+ * />
64
+ * ```
65
+ */
66
+ declare const SlopVideo: React.FC<SlopVideoProps>;
67
+
68
+ export { SlopImage, type SlopImageProps, SlopVideo, type SlopVideoProps };
package/dist/index.js CHANGED
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- SlopImage: () => SlopImage
33
+ SlopImage: () => SlopImage,
34
+ SlopVideo: () => SlopVideo
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
36
37
 
@@ -288,7 +289,283 @@ var SlopImage = ({
288
289
  )
289
290
  ] });
290
291
  };
292
+
293
+ // src/components/SlopVideo.tsx
294
+ var import_react2 = require("react");
295
+ var import_clsx2 = require("clsx");
296
+ var import_tailwind_merge2 = require("tailwind-merge");
297
+ var import_core2 = require("@slopmachine/core");
298
+ var import_jsx_runtime2 = require("react/jsx-runtime");
299
+ function cn2(...inputs) {
300
+ return (0, import_tailwind_merge2.twMerge)((0, import_clsx2.clsx)(inputs));
301
+ }
302
+ var SlopVideo = ({
303
+ bucketId,
304
+ className,
305
+ aspectRatio = "16:9",
306
+ version,
307
+ resultId,
308
+ model,
309
+ variables = {},
310
+ duration,
311
+ baseUrl,
312
+ loader,
313
+ autoPlay = true,
314
+ loop = true,
315
+ muted = true,
316
+ playsInline = true,
317
+ ...props
318
+ }) => {
319
+ const rawSrc = (0, import_react2.useMemo)(
320
+ () => (0, import_core2.buildVideoUrl)({
321
+ bucketId,
322
+ aspectRatio,
323
+ version,
324
+ resultId,
325
+ variables,
326
+ duration,
327
+ baseUrl,
328
+ model
329
+ }),
330
+ [
331
+ bucketId,
332
+ aspectRatio,
333
+ version,
334
+ resultId,
335
+ variables,
336
+ duration,
337
+ baseUrl,
338
+ model
339
+ ]
340
+ );
341
+ const [src, setSrc] = (0, import_react2.useState)(rawSrc);
342
+ (0, import_react2.useEffect)(() => {
343
+ const timer = setTimeout(() => {
344
+ setSrc(rawSrc);
345
+ }, 100);
346
+ return () => clearTimeout(timer);
347
+ }, [rawSrc]);
348
+ const [isLoading, setIsLoading] = (0, import_react2.useState)(true);
349
+ const [currentSrc, setCurrentSrc] = (0, import_react2.useState)(src);
350
+ const videoRef = (0, import_react2.useRef)(null);
351
+ if (src !== currentSrc) {
352
+ setCurrentSrc(src);
353
+ setIsLoading(true);
354
+ }
355
+ (0, import_react2.useEffect)(() => {
356
+ if (!src) return;
357
+ fetch(src, { method: "HEAD" }).then(async (res) => {
358
+ if (!res.ok) {
359
+ try {
360
+ const errRes = await fetch(src);
361
+ const errJson = await errRes.json();
362
+ console.error("SlopVideo error:", res.status, errJson.error);
363
+ } catch (e) {
364
+ }
365
+ setIsLoading(false);
366
+ }
367
+ }).catch((err) => {
368
+ console.error(`Error fetching video from ${src}:`, err);
369
+ setIsLoading(false);
370
+ });
371
+ }, [src]);
372
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
373
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `
374
+ @keyframes slop-shimmer {
375
+ 0% {
376
+ background-position: 200% 0;
377
+ }
378
+ 100% {
379
+ background-position: -200% 0;
380
+ }
381
+ }
382
+ @keyframes slop-spin {
383
+ from {
384
+ transform: rotate(0deg);
385
+ }
386
+ to {
387
+ transform: rotate(360deg);
388
+ }
389
+ }
390
+ ` }),
391
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
392
+ "div",
393
+ {
394
+ className: cn2("relative overflow-hidden", className),
395
+ style: {
396
+ aspectRatio: String(aspectRatio).replace(":", "/"),
397
+ backgroundColor: "var(--muted, #f3f4f6)",
398
+ position: "relative",
399
+ overflow: "hidden"
400
+ },
401
+ children: [
402
+ loader ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
403
+ "div",
404
+ {
405
+ className: cn2(
406
+ "absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
407
+ isLoading ? "opacity-100" : "opacity-0 pointer-events-none"
408
+ ),
409
+ style: {
410
+ position: "absolute",
411
+ top: 0,
412
+ left: 0,
413
+ right: 0,
414
+ bottom: 0,
415
+ zIndex: 10,
416
+ display: "flex",
417
+ alignItems: "center",
418
+ justifyContent: "center",
419
+ opacity: isLoading ? 1 : 0,
420
+ pointerEvents: isLoading ? "auto" : "none",
421
+ transition: "opacity 300ms ease-in-out"
422
+ },
423
+ children: loader
424
+ }
425
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
426
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
427
+ "div",
428
+ {
429
+ className: cn2(
430
+ "absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
431
+ isLoading ? "opacity-100" : "opacity-0 pointer-events-none"
432
+ ),
433
+ style: {
434
+ position: "absolute",
435
+ top: 0,
436
+ left: 0,
437
+ right: 0,
438
+ bottom: 0,
439
+ zIndex: 10,
440
+ display: "flex",
441
+ alignItems: "center",
442
+ justifyContent: "center",
443
+ backgroundColor: "var(--muted, #f3f4f6)",
444
+ opacity: isLoading ? 1 : 0,
445
+ pointerEvents: isLoading ? "auto" : "none",
446
+ transition: "opacity 300ms ease-in-out"
447
+ },
448
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
449
+ "div",
450
+ {
451
+ className: "flex flex-col items-center gap-2",
452
+ style: {
453
+ display: "flex",
454
+ flexDirection: "column",
455
+ alignItems: "center",
456
+ gap: "0.5rem"
457
+ },
458
+ children: [
459
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
460
+ "svg",
461
+ {
462
+ className: "spinner size-6 text-muted-foreground",
463
+ xmlns: "http://www.w3.org/2000/svg",
464
+ fill: "none",
465
+ viewBox: "0 0 24 24",
466
+ style: {
467
+ width: "24px",
468
+ height: "24px",
469
+ color: "var(--muted-foreground, #6b7280)",
470
+ animation: "slop-spin 1s linear infinite"
471
+ },
472
+ children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
474
+ "circle",
475
+ {
476
+ className: "opacity-25",
477
+ cx: "12",
478
+ cy: "12",
479
+ r: "10",
480
+ stroke: "currentColor",
481
+ strokeWidth: "4",
482
+ style: { opacity: 0.25 }
483
+ }
484
+ ),
485
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
486
+ "path",
487
+ {
488
+ className: "opacity-75",
489
+ fill: "currentColor",
490
+ 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",
491
+ style: { opacity: 0.75 }
492
+ }
493
+ )
494
+ ]
495
+ }
496
+ ),
497
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
498
+ "span",
499
+ {
500
+ className: "text-xs text-muted-foreground",
501
+ style: {
502
+ fontSize: "0.75rem",
503
+ color: "var(--muted-foreground, #6b7280)"
504
+ },
505
+ children: "Loading..."
506
+ }
507
+ )
508
+ ]
509
+ }
510
+ )
511
+ }
512
+ ),
513
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
514
+ "div",
515
+ {
516
+ className: cn2(
517
+ "shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
518
+ isLoading ? "opacity-100" : "opacity-0"
519
+ ),
520
+ style: {
521
+ position: "absolute",
522
+ top: 0,
523
+ left: 0,
524
+ right: 0,
525
+ bottom: 0,
526
+ background: "linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
527
+ backgroundSize: "200% 100%",
528
+ animation: "slop-shimmer 2s ease-in-out infinite",
529
+ opacity: isLoading ? 1 : 0,
530
+ transition: "opacity 300ms"
531
+ }
532
+ }
533
+ )
534
+ ] }),
535
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
536
+ "video",
537
+ {
538
+ ref: videoRef,
539
+ src,
540
+ autoPlay,
541
+ loop,
542
+ muted,
543
+ playsInline,
544
+ onLoadedData: () => setIsLoading(false),
545
+ onError: () => setIsLoading(false),
546
+ className: cn2(
547
+ "h-full w-full object-cover transition-opacity duration-500",
548
+ isLoading ? "opacity-0" : "opacity-100"
549
+ ),
550
+ style: {
551
+ height: "100%",
552
+ width: "100%",
553
+ objectFit: "cover",
554
+ transition: "opacity 500ms",
555
+ opacity: isLoading ? 0 : 1
556
+ },
557
+ ...props
558
+ },
559
+ src
560
+ )
561
+ ]
562
+ }
563
+ )
564
+ ] });
565
+ };
291
566
  // Annotate the CommonJS export names for ESM import in node:
292
567
  0 && (module.exports = {
293
- SlopImage
568
+ SlopImage,
569
+ SlopVideo
294
570
  });
571
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/components/SlopImage.tsx","../src/components/SlopVideo.tsx"],"sourcesContent":["export { SlopImage, type SlopImageProps } from \"./components/SlopImage\";\nexport { SlopVideo, type SlopVideoProps } from \"./components/SlopVideo\";\nexport type {\n ImageAspectRatio,\n VideoAspectRatio,\n SlopImageOptions,\n SlopVideoOptions,\n} from \"@slopmachine/core\";\n","import React, { useMemo, useState } from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nimport {\n buildImageUrl,\n type SlopImageOptions,\n type ImageAspectRatio,\n} from \"@slopmachine/core\";\n\n// Utility for Tailwind classes\nfunction cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport interface SlopImageProps\n extends\n Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"alt\">,\n Omit<SlopImageOptions, \"aspectRatio\"> {\n /**\n * The aspect ratio of the generated image. Defaults to \"1:1\".\n */\n aspectRatio?: ImageAspectRatio;\n /**\n * Custom React node to display while the image is loading.\n * If not provided, a default spinner and shimmer effect will be shown.\n */\n loader?: React.ReactNode;\n}\n\n/**\n * Renders an image generated by Slop Machine.\n *\n * This component automatically constructs the correct URL for the Slop Machine API,\n * displays a loading skeleton or custom loader while fetching, and handles errors.\n *\n * @example\n * ```tsx\n * <SlopImage\n * bucketId=\"my-bucket\"\n * resultId=\"some-result\"\n * aspectRatio=\"16:9\"\n * className=\"rounded-lg\"\n * />\n * ```\n *\n * @version 0.1.12\n */\nexport const SlopImage: React.FC<SlopImageProps> = ({\n bucketId,\n className,\n aspectRatio = \"1:1\",\n version,\n resultId,\n model,\n variables = {},\n baseUrl,\n loader,\n ...props\n}) => {\n // Construct API URL\n const rawSrc = useMemo(\n () =>\n buildImageUrl({\n bucketId,\n aspectRatio,\n version,\n resultId,\n variables,\n baseUrl,\n model,\n }),\n [bucketId, aspectRatio, version, resultId, variables, baseUrl, model],\n );\n\n const [src, setSrc] = useState(rawSrc);\n\n React.useEffect(() => {\n const timer = setTimeout(() => {\n setSrc(rawSrc);\n }, 100);\n return () => clearTimeout(timer);\n }, [rawSrc]);\n\n const alt = \"Generated image\";\n\n const [isLoading, setIsLoading] = useState(true);\n const [currentSrc, setCurrentSrc] = useState(src);\n\n if (src !== currentSrc) {\n setCurrentSrc(src);\n setIsLoading(true);\n }\n\n React.useEffect(() => {\n if (!src) return;\n\n fetch(src, { method: \"HEAD\" })\n .then(async (res) => {\n if (!res.ok) {\n // console.error(\n // `Failed to load image from ${src}. Status: ${res.status} ${res.statusText}`,\n // );\n try {\n const errRes = await fetch(src);\n const errJson = await errRes.json();\n console.error(\"SlopImage error:\", res.status, errJson.error);\n } catch (e) {\n // Failed to fetch or parse error details\n }\n setIsLoading(false);\n }\n })\n .catch((err) => {\n console.error(`Error fetching image from ${src}:`, err);\n setIsLoading(false);\n });\n }, [src]);\n\n return (\n <>\n <style>{`\n @keyframes slop-shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n }\n @keyframes slop-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `}</style>\n <div\n className={cn(\"relative overflow-hidden\", className)}\n style={{\n aspectRatio: String(aspectRatio).replace(\":\", \"/\"),\n backgroundColor: \"var(--muted, #f3f4f6)\",\n position: \"relative\",\n overflow: \"hidden\",\n }}\n >\n {/* Loading UI */}\n {loader ? (\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n {loader}\n </div>\n ) : (\n <>\n {/* Loading overlay */}\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: \"var(--muted, #f3f4f6)\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n <div\n className=\"flex flex-col items-center gap-2\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"0.5rem\",\n }}\n >\n <svg\n className=\"spinner size-6 text-muted-foreground\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n style={{\n width: \"24px\",\n height: \"24px\",\n color: \"var(--muted-foreground, #6b7280)\",\n animation: \"slop-spin 1s linear infinite\",\n }}\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n style={{ opacity: 0.25 }}\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n 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\"\n style={{ opacity: 0.75 }}\n />\n </svg>\n <span\n className=\"text-xs text-muted-foreground\"\n style={{\n fontSize: \"0.75rem\",\n color: \"var(--muted-foreground, #6b7280)\",\n }}\n >\n Loading...\n </span>\n </div>\n </div>\n\n {/* Shimmer effect underneath */}\n <div\n className={cn(\n \"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted\",\n isLoading ? \"opacity-100\" : \"opacity-0\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n background:\n \"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)\",\n backgroundSize: \"200% 100%\",\n animation: \"slop-shimmer 2s ease-in-out infinite\",\n opacity: isLoading ? 1 : 0,\n transition: \"opacity 300ms\",\n }}\n />\n </>\n )}\n\n {/* Image */}\n <img\n key={src}\n src={src}\n alt={alt}\n onLoad={() => setIsLoading(false)}\n onError={() => setIsLoading(false)}\n className={cn(\n \"h-full w-full object-cover transition-opacity duration-500\",\n isLoading ? \"opacity-0\" : \"opacity-100\",\n )}\n style={{\n height: \"100%\",\n width: \"100%\",\n objectFit: \"cover\",\n transition: \"opacity 500ms\",\n opacity: isLoading ? 0 : 1,\n }}\n {...props}\n />\n </div>\n </>\n );\n};\n","import React, { useMemo, useState, useRef, useEffect } from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nimport {\n buildVideoUrl,\n type SlopVideoOptions,\n type VideoAspectRatio,\n} from \"@slopmachine/core\";\n\n// Utility for Tailwind classes\nfunction cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport interface SlopVideoProps\n extends\n Omit<React.VideoHTMLAttributes<HTMLVideoElement>, \"src\">,\n Omit<SlopVideoOptions, \"aspectRatio\"> {\n /**\n * The aspect ratio of the generated video. Defaults to \"16:9\".\n */\n aspectRatio?: VideoAspectRatio;\n /**\n * Custom React node to display while the video is loading.\n * If not provided, a default spinner and shimmer effect will be shown.\n */\n loader?: React.ReactNode;\n}\n\n/**\n * Renders a video generated by Slop Machine.\n *\n * This component automatically constructs the correct URL for the Slop Machine API,\n * displays a loading skeleton or custom loader while fetching, and handles errors.\n *\n * @example\n * ```tsx\n * <SlopVideo\n * bucketId=\"my-bucket\"\n * resultId=\"some-result\"\n * aspectRatio=\"16:9\"\n * className=\"rounded-lg\"\n * autoPlay\n * loop\n * muted\n * />\n * ```\n */\nexport const SlopVideo: React.FC<SlopVideoProps> = ({\n bucketId,\n className,\n aspectRatio = \"16:9\",\n version,\n resultId,\n model,\n variables = {},\n duration,\n baseUrl,\n loader,\n autoPlay = true,\n loop = true,\n muted = true,\n playsInline = true,\n ...props\n}) => {\n // Construct API URL\n const rawSrc = useMemo(\n () =>\n buildVideoUrl({\n bucketId,\n aspectRatio,\n version,\n resultId,\n variables,\n duration,\n baseUrl,\n model,\n }),\n [\n bucketId,\n aspectRatio,\n version,\n resultId,\n variables,\n duration,\n baseUrl,\n model,\n ],\n );\n\n const [src, setSrc] = useState(rawSrc);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setSrc(rawSrc);\n }, 100);\n return () => clearTimeout(timer);\n }, [rawSrc]);\n\n const [isLoading, setIsLoading] = useState(true);\n const [currentSrc, setCurrentSrc] = useState(src);\n const videoRef = useRef<HTMLVideoElement>(null);\n\n if (src !== currentSrc) {\n setCurrentSrc(src);\n setIsLoading(true);\n }\n\n useEffect(() => {\n if (!src) return;\n\n fetch(src, { method: \"HEAD\" })\n .then(async (res) => {\n if (!res.ok) {\n try {\n const errRes = await fetch(src);\n const errJson = await errRes.json();\n console.error(\"SlopVideo error:\", res.status, errJson.error);\n } catch (e) {\n // Failed to fetch or parse error details\n }\n setIsLoading(false);\n }\n })\n .catch((err) => {\n console.error(`Error fetching video from ${src}:`, err);\n setIsLoading(false);\n });\n }, [src]);\n\n return (\n <>\n <style>{`\n @keyframes slop-shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n }\n @keyframes slop-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `}</style>\n <div\n className={cn(\"relative overflow-hidden\", className)}\n style={{\n aspectRatio: String(aspectRatio).replace(\":\", \"/\"),\n backgroundColor: \"var(--muted, #f3f4f6)\",\n position: \"relative\",\n overflow: \"hidden\",\n }}\n >\n {/* Loading UI */}\n {loader ? (\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n {loader}\n </div>\n ) : (\n <>\n {/* Loading overlay */}\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: \"var(--muted, #f3f4f6)\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n <div\n className=\"flex flex-col items-center gap-2\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"0.5rem\",\n }}\n >\n <svg\n className=\"spinner size-6 text-muted-foreground\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n style={{\n width: \"24px\",\n height: \"24px\",\n color: \"var(--muted-foreground, #6b7280)\",\n animation: \"slop-spin 1s linear infinite\",\n }}\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n style={{ opacity: 0.25 }}\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n 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\"\n style={{ opacity: 0.75 }}\n />\n </svg>\n <span\n className=\"text-xs text-muted-foreground\"\n style={{\n fontSize: \"0.75rem\",\n color: \"var(--muted-foreground, #6b7280)\",\n }}\n >\n Loading...\n </span>\n </div>\n </div>\n\n {/* Shimmer effect underneath */}\n <div\n className={cn(\n \"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted\",\n isLoading ? \"opacity-100\" : \"opacity-0\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n background:\n \"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)\",\n backgroundSize: \"200% 100%\",\n animation: \"slop-shimmer 2s ease-in-out infinite\",\n opacity: isLoading ? 1 : 0,\n transition: \"opacity 300ms\",\n }}\n />\n </>\n )}\n\n {/* Video */}\n <video\n key={src}\n ref={videoRef}\n src={src}\n autoPlay={autoPlay}\n loop={loop}\n muted={muted}\n playsInline={playsInline}\n onLoadedData={() => setIsLoading(false)}\n onError={() => setIsLoading(false)}\n className={cn(\n \"h-full w-full object-cover transition-opacity duration-500\",\n isLoading ? \"opacity-0\" : \"opacity-100\",\n )}\n style={{\n height: \"100%\",\n width: \"100%\",\n objectFit: \"cover\",\n transition: \"opacity 500ms\",\n opacity: isLoading ? 0 : 1,\n }}\n {...props}\n />\n </div>\n </>\n );\n};\n"],"mappings":"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","names":["React","import_react","import_clsx","import_tailwind_merge","import_core","import_jsx_runtime","cn"]}
package/dist/index.mjs CHANGED
@@ -2,7 +2,9 @@
2
2
  import React, { useMemo, useState } from "react";
3
3
  import { clsx } from "clsx";
4
4
  import { twMerge } from "tailwind-merge";
5
- import { buildImageUrl } from "@slopmachine/core";
5
+ import {
6
+ buildImageUrl
7
+ } from "@slopmachine/core";
6
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
9
  function cn(...inputs) {
8
10
  return twMerge(clsx(inputs));
@@ -252,6 +254,284 @@ var SlopImage = ({
252
254
  )
253
255
  ] });
254
256
  };
257
+
258
+ // src/components/SlopVideo.tsx
259
+ import { useMemo as useMemo2, useState as useState2, useRef, useEffect } from "react";
260
+ import { clsx as clsx2 } from "clsx";
261
+ import { twMerge as twMerge2 } from "tailwind-merge";
262
+ import {
263
+ buildVideoUrl
264
+ } from "@slopmachine/core";
265
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
266
+ function cn2(...inputs) {
267
+ return twMerge2(clsx2(inputs));
268
+ }
269
+ var SlopVideo = ({
270
+ bucketId,
271
+ className,
272
+ aspectRatio = "16:9",
273
+ version,
274
+ resultId,
275
+ model,
276
+ variables = {},
277
+ duration,
278
+ baseUrl,
279
+ loader,
280
+ autoPlay = true,
281
+ loop = true,
282
+ muted = true,
283
+ playsInline = true,
284
+ ...props
285
+ }) => {
286
+ const rawSrc = useMemo2(
287
+ () => buildVideoUrl({
288
+ bucketId,
289
+ aspectRatio,
290
+ version,
291
+ resultId,
292
+ variables,
293
+ duration,
294
+ baseUrl,
295
+ model
296
+ }),
297
+ [
298
+ bucketId,
299
+ aspectRatio,
300
+ version,
301
+ resultId,
302
+ variables,
303
+ duration,
304
+ baseUrl,
305
+ model
306
+ ]
307
+ );
308
+ const [src, setSrc] = useState2(rawSrc);
309
+ useEffect(() => {
310
+ const timer = setTimeout(() => {
311
+ setSrc(rawSrc);
312
+ }, 100);
313
+ return () => clearTimeout(timer);
314
+ }, [rawSrc]);
315
+ const [isLoading, setIsLoading] = useState2(true);
316
+ const [currentSrc, setCurrentSrc] = useState2(src);
317
+ const videoRef = useRef(null);
318
+ if (src !== currentSrc) {
319
+ setCurrentSrc(src);
320
+ setIsLoading(true);
321
+ }
322
+ useEffect(() => {
323
+ if (!src) return;
324
+ fetch(src, { method: "HEAD" }).then(async (res) => {
325
+ if (!res.ok) {
326
+ try {
327
+ const errRes = await fetch(src);
328
+ const errJson = await errRes.json();
329
+ console.error("SlopVideo error:", res.status, errJson.error);
330
+ } catch (e) {
331
+ }
332
+ setIsLoading(false);
333
+ }
334
+ }).catch((err) => {
335
+ console.error(`Error fetching video from ${src}:`, err);
336
+ setIsLoading(false);
337
+ });
338
+ }, [src]);
339
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
340
+ /* @__PURE__ */ jsx2("style", { children: `
341
+ @keyframes slop-shimmer {
342
+ 0% {
343
+ background-position: 200% 0;
344
+ }
345
+ 100% {
346
+ background-position: -200% 0;
347
+ }
348
+ }
349
+ @keyframes slop-spin {
350
+ from {
351
+ transform: rotate(0deg);
352
+ }
353
+ to {
354
+ transform: rotate(360deg);
355
+ }
356
+ }
357
+ ` }),
358
+ /* @__PURE__ */ jsxs2(
359
+ "div",
360
+ {
361
+ className: cn2("relative overflow-hidden", className),
362
+ style: {
363
+ aspectRatio: String(aspectRatio).replace(":", "/"),
364
+ backgroundColor: "var(--muted, #f3f4f6)",
365
+ position: "relative",
366
+ overflow: "hidden"
367
+ },
368
+ children: [
369
+ loader ? /* @__PURE__ */ jsx2(
370
+ "div",
371
+ {
372
+ className: cn2(
373
+ "absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
374
+ isLoading ? "opacity-100" : "opacity-0 pointer-events-none"
375
+ ),
376
+ style: {
377
+ position: "absolute",
378
+ top: 0,
379
+ left: 0,
380
+ right: 0,
381
+ bottom: 0,
382
+ zIndex: 10,
383
+ display: "flex",
384
+ alignItems: "center",
385
+ justifyContent: "center",
386
+ opacity: isLoading ? 1 : 0,
387
+ pointerEvents: isLoading ? "auto" : "none",
388
+ transition: "opacity 300ms ease-in-out"
389
+ },
390
+ children: loader
391
+ }
392
+ ) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
393
+ /* @__PURE__ */ jsx2(
394
+ "div",
395
+ {
396
+ className: cn2(
397
+ "absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
398
+ isLoading ? "opacity-100" : "opacity-0 pointer-events-none"
399
+ ),
400
+ style: {
401
+ position: "absolute",
402
+ top: 0,
403
+ left: 0,
404
+ right: 0,
405
+ bottom: 0,
406
+ zIndex: 10,
407
+ display: "flex",
408
+ alignItems: "center",
409
+ justifyContent: "center",
410
+ backgroundColor: "var(--muted, #f3f4f6)",
411
+ opacity: isLoading ? 1 : 0,
412
+ pointerEvents: isLoading ? "auto" : "none",
413
+ transition: "opacity 300ms ease-in-out"
414
+ },
415
+ children: /* @__PURE__ */ jsxs2(
416
+ "div",
417
+ {
418
+ className: "flex flex-col items-center gap-2",
419
+ style: {
420
+ display: "flex",
421
+ flexDirection: "column",
422
+ alignItems: "center",
423
+ gap: "0.5rem"
424
+ },
425
+ children: [
426
+ /* @__PURE__ */ jsxs2(
427
+ "svg",
428
+ {
429
+ className: "spinner size-6 text-muted-foreground",
430
+ xmlns: "http://www.w3.org/2000/svg",
431
+ fill: "none",
432
+ viewBox: "0 0 24 24",
433
+ style: {
434
+ width: "24px",
435
+ height: "24px",
436
+ color: "var(--muted-foreground, #6b7280)",
437
+ animation: "slop-spin 1s linear infinite"
438
+ },
439
+ children: [
440
+ /* @__PURE__ */ jsx2(
441
+ "circle",
442
+ {
443
+ className: "opacity-25",
444
+ cx: "12",
445
+ cy: "12",
446
+ r: "10",
447
+ stroke: "currentColor",
448
+ strokeWidth: "4",
449
+ style: { opacity: 0.25 }
450
+ }
451
+ ),
452
+ /* @__PURE__ */ jsx2(
453
+ "path",
454
+ {
455
+ className: "opacity-75",
456
+ fill: "currentColor",
457
+ 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",
458
+ style: { opacity: 0.75 }
459
+ }
460
+ )
461
+ ]
462
+ }
463
+ ),
464
+ /* @__PURE__ */ jsx2(
465
+ "span",
466
+ {
467
+ className: "text-xs text-muted-foreground",
468
+ style: {
469
+ fontSize: "0.75rem",
470
+ color: "var(--muted-foreground, #6b7280)"
471
+ },
472
+ children: "Loading..."
473
+ }
474
+ )
475
+ ]
476
+ }
477
+ )
478
+ }
479
+ ),
480
+ /* @__PURE__ */ jsx2(
481
+ "div",
482
+ {
483
+ className: cn2(
484
+ "shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
485
+ isLoading ? "opacity-100" : "opacity-0"
486
+ ),
487
+ style: {
488
+ position: "absolute",
489
+ top: 0,
490
+ left: 0,
491
+ right: 0,
492
+ bottom: 0,
493
+ background: "linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
494
+ backgroundSize: "200% 100%",
495
+ animation: "slop-shimmer 2s ease-in-out infinite",
496
+ opacity: isLoading ? 1 : 0,
497
+ transition: "opacity 300ms"
498
+ }
499
+ }
500
+ )
501
+ ] }),
502
+ /* @__PURE__ */ jsx2(
503
+ "video",
504
+ {
505
+ ref: videoRef,
506
+ src,
507
+ autoPlay,
508
+ loop,
509
+ muted,
510
+ playsInline,
511
+ onLoadedData: () => setIsLoading(false),
512
+ onError: () => setIsLoading(false),
513
+ className: cn2(
514
+ "h-full w-full object-cover transition-opacity duration-500",
515
+ isLoading ? "opacity-0" : "opacity-100"
516
+ ),
517
+ style: {
518
+ height: "100%",
519
+ width: "100%",
520
+ objectFit: "cover",
521
+ transition: "opacity 500ms",
522
+ opacity: isLoading ? 0 : 1
523
+ },
524
+ ...props
525
+ },
526
+ src
527
+ )
528
+ ]
529
+ }
530
+ )
531
+ ] });
532
+ };
255
533
  export {
256
- SlopImage
534
+ SlopImage,
535
+ SlopVideo
257
536
  };
537
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/SlopImage.tsx","../src/components/SlopVideo.tsx"],"sourcesContent":["import React, { useMemo, useState } from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nimport {\n buildImageUrl,\n type SlopImageOptions,\n type ImageAspectRatio,\n} from \"@slopmachine/core\";\n\n// Utility for Tailwind classes\nfunction cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport interface SlopImageProps\n extends\n Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"alt\">,\n Omit<SlopImageOptions, \"aspectRatio\"> {\n /**\n * The aspect ratio of the generated image. Defaults to \"1:1\".\n */\n aspectRatio?: ImageAspectRatio;\n /**\n * Custom React node to display while the image is loading.\n * If not provided, a default spinner and shimmer effect will be shown.\n */\n loader?: React.ReactNode;\n}\n\n/**\n * Renders an image generated by Slop Machine.\n *\n * This component automatically constructs the correct URL for the Slop Machine API,\n * displays a loading skeleton or custom loader while fetching, and handles errors.\n *\n * @example\n * ```tsx\n * <SlopImage\n * bucketId=\"my-bucket\"\n * resultId=\"some-result\"\n * aspectRatio=\"16:9\"\n * className=\"rounded-lg\"\n * />\n * ```\n *\n * @version 0.1.12\n */\nexport const SlopImage: React.FC<SlopImageProps> = ({\n bucketId,\n className,\n aspectRatio = \"1:1\",\n version,\n resultId,\n model,\n variables = {},\n baseUrl,\n loader,\n ...props\n}) => {\n // Construct API URL\n const rawSrc = useMemo(\n () =>\n buildImageUrl({\n bucketId,\n aspectRatio,\n version,\n resultId,\n variables,\n baseUrl,\n model,\n }),\n [bucketId, aspectRatio, version, resultId, variables, baseUrl, model],\n );\n\n const [src, setSrc] = useState(rawSrc);\n\n React.useEffect(() => {\n const timer = setTimeout(() => {\n setSrc(rawSrc);\n }, 100);\n return () => clearTimeout(timer);\n }, [rawSrc]);\n\n const alt = \"Generated image\";\n\n const [isLoading, setIsLoading] = useState(true);\n const [currentSrc, setCurrentSrc] = useState(src);\n\n if (src !== currentSrc) {\n setCurrentSrc(src);\n setIsLoading(true);\n }\n\n React.useEffect(() => {\n if (!src) return;\n\n fetch(src, { method: \"HEAD\" })\n .then(async (res) => {\n if (!res.ok) {\n // console.error(\n // `Failed to load image from ${src}. Status: ${res.status} ${res.statusText}`,\n // );\n try {\n const errRes = await fetch(src);\n const errJson = await errRes.json();\n console.error(\"SlopImage error:\", res.status, errJson.error);\n } catch (e) {\n // Failed to fetch or parse error details\n }\n setIsLoading(false);\n }\n })\n .catch((err) => {\n console.error(`Error fetching image from ${src}:`, err);\n setIsLoading(false);\n });\n }, [src]);\n\n return (\n <>\n <style>{`\n @keyframes slop-shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n }\n @keyframes slop-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `}</style>\n <div\n className={cn(\"relative overflow-hidden\", className)}\n style={{\n aspectRatio: String(aspectRatio).replace(\":\", \"/\"),\n backgroundColor: \"var(--muted, #f3f4f6)\",\n position: \"relative\",\n overflow: \"hidden\",\n }}\n >\n {/* Loading UI */}\n {loader ? (\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n {loader}\n </div>\n ) : (\n <>\n {/* Loading overlay */}\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: \"var(--muted, #f3f4f6)\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n <div\n className=\"flex flex-col items-center gap-2\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"0.5rem\",\n }}\n >\n <svg\n className=\"spinner size-6 text-muted-foreground\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n style={{\n width: \"24px\",\n height: \"24px\",\n color: \"var(--muted-foreground, #6b7280)\",\n animation: \"slop-spin 1s linear infinite\",\n }}\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n style={{ opacity: 0.25 }}\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n 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\"\n style={{ opacity: 0.75 }}\n />\n </svg>\n <span\n className=\"text-xs text-muted-foreground\"\n style={{\n fontSize: \"0.75rem\",\n color: \"var(--muted-foreground, #6b7280)\",\n }}\n >\n Loading...\n </span>\n </div>\n </div>\n\n {/* Shimmer effect underneath */}\n <div\n className={cn(\n \"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted\",\n isLoading ? \"opacity-100\" : \"opacity-0\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n background:\n \"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)\",\n backgroundSize: \"200% 100%\",\n animation: \"slop-shimmer 2s ease-in-out infinite\",\n opacity: isLoading ? 1 : 0,\n transition: \"opacity 300ms\",\n }}\n />\n </>\n )}\n\n {/* Image */}\n <img\n key={src}\n src={src}\n alt={alt}\n onLoad={() => setIsLoading(false)}\n onError={() => setIsLoading(false)}\n className={cn(\n \"h-full w-full object-cover transition-opacity duration-500\",\n isLoading ? \"opacity-0\" : \"opacity-100\",\n )}\n style={{\n height: \"100%\",\n width: \"100%\",\n objectFit: \"cover\",\n transition: \"opacity 500ms\",\n opacity: isLoading ? 0 : 1,\n }}\n {...props}\n />\n </div>\n </>\n );\n};\n","import React, { useMemo, useState, useRef, useEffect } from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nimport {\n buildVideoUrl,\n type SlopVideoOptions,\n type VideoAspectRatio,\n} from \"@slopmachine/core\";\n\n// Utility for Tailwind classes\nfunction cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport interface SlopVideoProps\n extends\n Omit<React.VideoHTMLAttributes<HTMLVideoElement>, \"src\">,\n Omit<SlopVideoOptions, \"aspectRatio\"> {\n /**\n * The aspect ratio of the generated video. Defaults to \"16:9\".\n */\n aspectRatio?: VideoAspectRatio;\n /**\n * Custom React node to display while the video is loading.\n * If not provided, a default spinner and shimmer effect will be shown.\n */\n loader?: React.ReactNode;\n}\n\n/**\n * Renders a video generated by Slop Machine.\n *\n * This component automatically constructs the correct URL for the Slop Machine API,\n * displays a loading skeleton or custom loader while fetching, and handles errors.\n *\n * @example\n * ```tsx\n * <SlopVideo\n * bucketId=\"my-bucket\"\n * resultId=\"some-result\"\n * aspectRatio=\"16:9\"\n * className=\"rounded-lg\"\n * autoPlay\n * loop\n * muted\n * />\n * ```\n */\nexport const SlopVideo: React.FC<SlopVideoProps> = ({\n bucketId,\n className,\n aspectRatio = \"16:9\",\n version,\n resultId,\n model,\n variables = {},\n duration,\n baseUrl,\n loader,\n autoPlay = true,\n loop = true,\n muted = true,\n playsInline = true,\n ...props\n}) => {\n // Construct API URL\n const rawSrc = useMemo(\n () =>\n buildVideoUrl({\n bucketId,\n aspectRatio,\n version,\n resultId,\n variables,\n duration,\n baseUrl,\n model,\n }),\n [\n bucketId,\n aspectRatio,\n version,\n resultId,\n variables,\n duration,\n baseUrl,\n model,\n ],\n );\n\n const [src, setSrc] = useState(rawSrc);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setSrc(rawSrc);\n }, 100);\n return () => clearTimeout(timer);\n }, [rawSrc]);\n\n const [isLoading, setIsLoading] = useState(true);\n const [currentSrc, setCurrentSrc] = useState(src);\n const videoRef = useRef<HTMLVideoElement>(null);\n\n if (src !== currentSrc) {\n setCurrentSrc(src);\n setIsLoading(true);\n }\n\n useEffect(() => {\n if (!src) return;\n\n fetch(src, { method: \"HEAD\" })\n .then(async (res) => {\n if (!res.ok) {\n try {\n const errRes = await fetch(src);\n const errJson = await errRes.json();\n console.error(\"SlopVideo error:\", res.status, errJson.error);\n } catch (e) {\n // Failed to fetch or parse error details\n }\n setIsLoading(false);\n }\n })\n .catch((err) => {\n console.error(`Error fetching video from ${src}:`, err);\n setIsLoading(false);\n });\n }, [src]);\n\n return (\n <>\n <style>{`\n @keyframes slop-shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n }\n @keyframes slop-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `}</style>\n <div\n className={cn(\"relative overflow-hidden\", className)}\n style={{\n aspectRatio: String(aspectRatio).replace(\":\", \"/\"),\n backgroundColor: \"var(--muted, #f3f4f6)\",\n position: \"relative\",\n overflow: \"hidden\",\n }}\n >\n {/* Loading UI */}\n {loader ? (\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n {loader}\n </div>\n ) : (\n <>\n {/* Loading overlay */}\n <div\n className={cn(\n \"absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300\",\n isLoading ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: \"var(--muted, #f3f4f6)\",\n opacity: isLoading ? 1 : 0,\n pointerEvents: isLoading ? \"auto\" : \"none\",\n transition: \"opacity 300ms ease-in-out\",\n }}\n >\n <div\n className=\"flex flex-col items-center gap-2\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"0.5rem\",\n }}\n >\n <svg\n className=\"spinner size-6 text-muted-foreground\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n style={{\n width: \"24px\",\n height: \"24px\",\n color: \"var(--muted-foreground, #6b7280)\",\n animation: \"slop-spin 1s linear infinite\",\n }}\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n style={{ opacity: 0.25 }}\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n 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\"\n style={{ opacity: 0.75 }}\n />\n </svg>\n <span\n className=\"text-xs text-muted-foreground\"\n style={{\n fontSize: \"0.75rem\",\n color: \"var(--muted-foreground, #6b7280)\",\n }}\n >\n Loading...\n </span>\n </div>\n </div>\n\n {/* Shimmer effect underneath */}\n <div\n className={cn(\n \"shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted\",\n isLoading ? \"opacity-100\" : \"opacity-0\",\n )}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n background:\n \"linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)\",\n backgroundSize: \"200% 100%\",\n animation: \"slop-shimmer 2s ease-in-out infinite\",\n opacity: isLoading ? 1 : 0,\n transition: \"opacity 300ms\",\n }}\n />\n </>\n )}\n\n {/* Video */}\n <video\n key={src}\n ref={videoRef}\n src={src}\n autoPlay={autoPlay}\n loop={loop}\n muted={muted}\n playsInline={playsInline}\n onLoadedData={() => setIsLoading(false)}\n onError={() => setIsLoading(false)}\n className={cn(\n \"h-full w-full object-cover transition-opacity duration-500\",\n isLoading ? \"opacity-0\" : \"opacity-100\",\n )}\n style={{\n height: \"100%\",\n width: \"100%\",\n objectFit: \"cover\",\n transition: \"opacity 500ms\",\n opacity: isLoading ? 0 : 1,\n }}\n {...props}\n />\n </div>\n </>\n );\n};\n"],"mappings":"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","names":["useMemo","useState","clsx","twMerge","Fragment","jsx","jsxs","cn"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slopmachine/react",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "The official React SDK for Slop Machine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -9,7 +9,8 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
12
+ "require": "./dist/index.js",
13
+ "default": "./dist/index.js"
13
14
  }
14
15
  },
15
16
  "scripts": {
@@ -2,20 +2,50 @@ import React, { useMemo, useState } from "react";
2
2
  import { clsx, type ClassValue } from "clsx";
3
3
  import { twMerge } from "tailwind-merge";
4
4
 
5
- import { buildImageUrl, type SlopImageOptions } from "@slopmachine/core";
5
+ import {
6
+ buildImageUrl,
7
+ type SlopImageOptions,
8
+ type ImageAspectRatio,
9
+ } from "@slopmachine/core";
6
10
 
7
11
  // Utility for Tailwind classes
8
12
  function cn(...inputs: ClassValue[]) {
9
13
  return twMerge(clsx(inputs));
10
14
  }
11
15
 
12
- interface SlopImageProps
16
+ export interface SlopImageProps
13
17
  extends
14
18
  Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">,
15
- SlopImageOptions {
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
+ */
16
28
  loader?: React.ReactNode;
17
29
  }
18
30
 
31
+ /**
32
+ * Renders an image generated by Slop Machine.
33
+ *
34
+ * This component automatically constructs the correct URL for the Slop Machine API,
35
+ * displays a loading skeleton or custom loader while fetching, and handles errors.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * <SlopImage
40
+ * bucketId="my-bucket"
41
+ * resultId="some-result"
42
+ * aspectRatio="16:9"
43
+ * className="rounded-lg"
44
+ * />
45
+ * ```
46
+ *
47
+ * @version 0.1.12
48
+ */
19
49
  export const SlopImage: React.FC<SlopImageProps> = ({
20
50
  bucketId,
21
51
  className,
@@ -0,0 +1,308 @@
1
+ import React, { useMemo, useState, useRef, useEffect } from "react";
2
+ import { clsx, type ClassValue } from "clsx";
3
+ import { twMerge } from "tailwind-merge";
4
+
5
+ import {
6
+ buildVideoUrl,
7
+ type SlopVideoOptions,
8
+ type VideoAspectRatio,
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 SlopVideoProps
17
+ extends
18
+ Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "src">,
19
+ Omit<SlopVideoOptions, "aspectRatio"> {
20
+ /**
21
+ * The aspect ratio of the generated video. Defaults to "16:9".
22
+ */
23
+ aspectRatio?: VideoAspectRatio;
24
+ /**
25
+ * Custom React node to display while the video is loading.
26
+ * If not provided, a default spinner and shimmer effect will be shown.
27
+ */
28
+ loader?: React.ReactNode;
29
+ }
30
+
31
+ /**
32
+ * Renders a video generated by Slop Machine.
33
+ *
34
+ * This component automatically constructs the correct URL for the Slop Machine API,
35
+ * displays a loading skeleton or custom loader while fetching, and handles errors.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * <SlopVideo
40
+ * bucketId="my-bucket"
41
+ * resultId="some-result"
42
+ * aspectRatio="16:9"
43
+ * className="rounded-lg"
44
+ * autoPlay
45
+ * loop
46
+ * muted
47
+ * />
48
+ * ```
49
+ */
50
+ export const SlopVideo: React.FC<SlopVideoProps> = ({
51
+ bucketId,
52
+ className,
53
+ aspectRatio = "16:9",
54
+ version,
55
+ resultId,
56
+ model,
57
+ variables = {},
58
+ duration,
59
+ baseUrl,
60
+ loader,
61
+ autoPlay = true,
62
+ loop = true,
63
+ muted = true,
64
+ playsInline = true,
65
+ ...props
66
+ }) => {
67
+ // Construct API URL
68
+ const rawSrc = useMemo(
69
+ () =>
70
+ buildVideoUrl({
71
+ bucketId,
72
+ aspectRatio,
73
+ version,
74
+ resultId,
75
+ variables,
76
+ duration,
77
+ baseUrl,
78
+ model,
79
+ }),
80
+ [
81
+ bucketId,
82
+ aspectRatio,
83
+ version,
84
+ resultId,
85
+ variables,
86
+ duration,
87
+ baseUrl,
88
+ model,
89
+ ],
90
+ );
91
+
92
+ const [src, setSrc] = useState(rawSrc);
93
+
94
+ useEffect(() => {
95
+ const timer = setTimeout(() => {
96
+ setSrc(rawSrc);
97
+ }, 100);
98
+ return () => clearTimeout(timer);
99
+ }, [rawSrc]);
100
+
101
+ const [isLoading, setIsLoading] = useState(true);
102
+ const [currentSrc, setCurrentSrc] = useState(src);
103
+ const videoRef = useRef<HTMLVideoElement>(null);
104
+
105
+ if (src !== currentSrc) {
106
+ setCurrentSrc(src);
107
+ setIsLoading(true);
108
+ }
109
+
110
+ useEffect(() => {
111
+ if (!src) return;
112
+
113
+ fetch(src, { method: "HEAD" })
114
+ .then(async (res) => {
115
+ if (!res.ok) {
116
+ try {
117
+ const errRes = await fetch(src);
118
+ const errJson = await errRes.json();
119
+ console.error("SlopVideo error:", res.status, errJson.error);
120
+ } catch (e) {
121
+ // Failed to fetch or parse error details
122
+ }
123
+ setIsLoading(false);
124
+ }
125
+ })
126
+ .catch((err) => {
127
+ console.error(`Error fetching video from ${src}:`, err);
128
+ setIsLoading(false);
129
+ });
130
+ }, [src]);
131
+
132
+ return (
133
+ <>
134
+ <style>{`
135
+ @keyframes slop-shimmer {
136
+ 0% {
137
+ background-position: 200% 0;
138
+ }
139
+ 100% {
140
+ background-position: -200% 0;
141
+ }
142
+ }
143
+ @keyframes slop-spin {
144
+ from {
145
+ transform: rotate(0deg);
146
+ }
147
+ to {
148
+ transform: rotate(360deg);
149
+ }
150
+ }
151
+ `}</style>
152
+ <div
153
+ className={cn("relative overflow-hidden", className)}
154
+ style={{
155
+ aspectRatio: String(aspectRatio).replace(":", "/"),
156
+ backgroundColor: "var(--muted, #f3f4f6)",
157
+ position: "relative",
158
+ overflow: "hidden",
159
+ }}
160
+ >
161
+ {/* Loading UI */}
162
+ {loader ? (
163
+ <div
164
+ className={cn(
165
+ "absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-300",
166
+ isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
167
+ )}
168
+ style={{
169
+ position: "absolute",
170
+ top: 0,
171
+ left: 0,
172
+ right: 0,
173
+ bottom: 0,
174
+ zIndex: 10,
175
+ display: "flex",
176
+ alignItems: "center",
177
+ justifyContent: "center",
178
+ opacity: isLoading ? 1 : 0,
179
+ pointerEvents: isLoading ? "auto" : "none",
180
+ transition: "opacity 300ms ease-in-out",
181
+ }}
182
+ >
183
+ {loader}
184
+ </div>
185
+ ) : (
186
+ <>
187
+ {/* Loading overlay */}
188
+ <div
189
+ className={cn(
190
+ "absolute inset-0 z-10 flex items-center justify-center bg-muted transition-opacity duration-300",
191
+ isLoading ? "opacity-100" : "opacity-0 pointer-events-none",
192
+ )}
193
+ style={{
194
+ position: "absolute",
195
+ top: 0,
196
+ left: 0,
197
+ right: 0,
198
+ bottom: 0,
199
+ zIndex: 10,
200
+ display: "flex",
201
+ alignItems: "center",
202
+ justifyContent: "center",
203
+ backgroundColor: "var(--muted, #f3f4f6)",
204
+ opacity: isLoading ? 1 : 0,
205
+ pointerEvents: isLoading ? "auto" : "none",
206
+ transition: "opacity 300ms ease-in-out",
207
+ }}
208
+ >
209
+ <div
210
+ className="flex flex-col items-center gap-2"
211
+ style={{
212
+ display: "flex",
213
+ flexDirection: "column",
214
+ alignItems: "center",
215
+ gap: "0.5rem",
216
+ }}
217
+ >
218
+ <svg
219
+ className="spinner size-6 text-muted-foreground"
220
+ xmlns="http://www.w3.org/2000/svg"
221
+ fill="none"
222
+ viewBox="0 0 24 24"
223
+ style={{
224
+ width: "24px",
225
+ height: "24px",
226
+ color: "var(--muted-foreground, #6b7280)",
227
+ animation: "slop-spin 1s linear infinite",
228
+ }}
229
+ >
230
+ <circle
231
+ className="opacity-25"
232
+ cx="12"
233
+ cy="12"
234
+ r="10"
235
+ stroke="currentColor"
236
+ strokeWidth="4"
237
+ style={{ opacity: 0.25 }}
238
+ />
239
+ <path
240
+ className="opacity-75"
241
+ fill="currentColor"
242
+ 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"
243
+ style={{ opacity: 0.75 }}
244
+ />
245
+ </svg>
246
+ <span
247
+ className="text-xs text-muted-foreground"
248
+ style={{
249
+ fontSize: "0.75rem",
250
+ color: "var(--muted-foreground, #6b7280)",
251
+ }}
252
+ >
253
+ Loading...
254
+ </span>
255
+ </div>
256
+ </div>
257
+
258
+ {/* Shimmer effect underneath */}
259
+ <div
260
+ className={cn(
261
+ "shimmer-effect absolute inset-0 bg-gradient-to-r from-muted via-muted/50 to-muted",
262
+ isLoading ? "opacity-100" : "opacity-0",
263
+ )}
264
+ style={{
265
+ position: "absolute",
266
+ top: 0,
267
+ left: 0,
268
+ right: 0,
269
+ bottom: 0,
270
+ background:
271
+ "linear-gradient(90deg, var(--muted, #f3f4f6) 0%, var(--muted-foreground, #e5e7eb) 50%, var(--muted, #f3f4f6) 100%)",
272
+ backgroundSize: "200% 100%",
273
+ animation: "slop-shimmer 2s ease-in-out infinite",
274
+ opacity: isLoading ? 1 : 0,
275
+ transition: "opacity 300ms",
276
+ }}
277
+ />
278
+ </>
279
+ )}
280
+
281
+ {/* Video */}
282
+ <video
283
+ key={src}
284
+ ref={videoRef}
285
+ src={src}
286
+ autoPlay={autoPlay}
287
+ loop={loop}
288
+ muted={muted}
289
+ playsInline={playsInline}
290
+ onLoadedData={() => setIsLoading(false)}
291
+ onError={() => setIsLoading(false)}
292
+ className={cn(
293
+ "h-full w-full object-cover transition-opacity duration-500",
294
+ isLoading ? "opacity-0" : "opacity-100",
295
+ )}
296
+ style={{
297
+ height: "100%",
298
+ width: "100%",
299
+ objectFit: "cover",
300
+ transition: "opacity 500ms",
301
+ opacity: isLoading ? 0 : 1,
302
+ }}
303
+ {...props}
304
+ />
305
+ </div>
306
+ </>
307
+ );
308
+ };
package/src/index.ts CHANGED
@@ -1 +1,8 @@
1
- export { SlopImage } from './components/SlopImage';
1
+ export { SlopImage, type SlopImageProps } from "./components/SlopImage";
2
+ export { SlopVideo, type SlopVideoProps } from "./components/SlopVideo";
3
+ export type {
4
+ ImageAspectRatio,
5
+ VideoAspectRatio,
6
+ SlopImageOptions,
7
+ SlopVideoOptions,
8
+ } from "@slopmachine/core";
package/tsconfig.json CHANGED
@@ -21,6 +21,7 @@
21
21
  "noFallthroughCasesInSwitch": true,
22
22
 
23
23
  "declaration": true,
24
+ "declarationMap": true,
24
25
  "baseUrl": "."
25
26
  },
26
27
  "include": ["src"],
package/tsup.config.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { defineConfig } from 'tsup';
1
+ import { defineConfig } from "tsup";
2
2
 
3
3
  export default defineConfig({
4
- entry: ['src/index.ts'],
5
- format: ['cjs', 'esm'],
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
6
  dts: true,
7
+ sourcemap: true,
7
8
  clean: true,
8
- external: ['react', 'react-dom'],
9
+ external: ["react", "react-dom"],
9
10
  });