@slopmachine/svelte 0.1.10 → 0.1.13

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,45 @@
1
+ # Svelte SDK Coding Conventions
2
+
3
+ This document outlines the coding conventions, architecture, and patterns used in the `@slopmachine/svelte` SDK package. Future agents and contributors should adhere to these guidelines when making changes.
4
+
5
+ ## 1. Svelte 5 Runes
6
+
7
+ The SDK utilizes **Svelte 5** and exclusively relies on **runes** for state management and reactivity.
8
+
9
+ - `$props()`: Used for defining component properties. Ensure default values are handled here, and `...restProps` is utilized to spread unused properties onto underlying DOM elements (e.g., `<img>`).
10
+ - `$state()`: Used for localized component state (e.g., loading states, URLs).
11
+ - `$derived()`: Used for computed values that depend on state or props (e.g., computing an image URL based on options).
12
+ - `$effect()`: Used for side effects, such as tracking prop changes over time, fetching, or debouncing.
13
+
14
+ ## 2. TypeScript and Typing
15
+
16
+ - All Svelte components must use `<script lang="ts">`.
17
+ - Props should be strictly typed. Interfaces should extend the base option types provided by the core package. For example, `SlopImageProps` extends `SlopImageOptions` from `@slopmachine/core`.
18
+ - Custom markup passed as props (e.g., a custom loader) should utilize the Svelte 5 `Snippet` type imported from `svelte`.
19
+
20
+ ## 3. Dependency Management
21
+
22
+ - **Core SDK Logic**: Do not duplicate core logic (like URL generation). Always import utility functions and base types from the `@slopmachine/core` package.
23
+ - **Peer Dependencies**: The Svelte package lists `svelte` (version `^5.0.0`) as a peer dependency.
24
+
25
+ ## 4. Component Structure
26
+
27
+ Components should follow a clear top-to-bottom structure:
28
+
29
+ 1. **Block Comments**: A descriptive JSDoc/HTML block comment at the top of the file outlining the `@component`, its description, `@example`, and `@version`.
30
+ 2. **Script Tag**: `<script lang="ts">` containing imports, type definitions, runes (`$props`, `$state`, etc.), and local functions.
31
+ 3. **Markup**: The Svelte HTML template using Svelte 5 logic blocks (`{#if}`, `{@render snippet()}`).
32
+ 4. **Styles**: A `<style>` tag scoped to the component. Use CSS variables with sensible fallbacks (e.g., `var(--muted, #f3f4f6)`) to allow consumer theming while maintaining a good default appearance.
33
+
34
+ ## 5. Exporting Patterns
35
+
36
+ - **No Bundling**: The package is published as uncompiled source code to let the consuming project's bundler handle the Svelte compilation.
37
+ - **package.json Setup**:
38
+ - Exposes `"type": "module"`.
39
+ - The `"svelte"`, `"main"`, and `"exports"` fields point directly to `./src/index.ts`.
40
+ - Component exports should be simple and direct via `src/index.ts`.
41
+
42
+ ## 6. Error Handling and Loading
43
+
44
+ - Implement clean fallbacks for failing resources (like broken image links). The `SlopImage` component demonstrates using `fetch` to proactively capture error statuses, falling back to an error state or logging.
45
+ - Support default loading indicators (spinners/shimmer effects) alongside customizable slots/snippets (e.g., `loader`) for a better user experience.
package/README.md CHANGED
@@ -14,13 +14,13 @@ pnpm add @slopmachine/svelte
14
14
 
15
15
  ## Demo
16
16
 
17
- https://slopmachine-dev.github.io/slopmachine-sdk/svelte/
17
+ https://slopmachine-dev.github.io/slopmachine-sdk/demo-svelte/
18
18
 
19
19
  ## Usage
20
20
 
21
21
  ```svelte
22
22
  <script lang="ts">
23
- import { SlopImage } from '@slopmachine/svelte';
23
+ import { SlopImage } from "@slopmachine/svelte";
24
24
  </script>
25
25
 
26
26
  <div style="width: 400px; height: 400px;">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slopmachine/svelte",
3
- "version": "0.1.10",
4
- "description": "Svelte wrapper for SlopMachine",
3
+ "version": "0.1.13",
4
+ "description": "The official Svelte SDK for Slop Machine",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "svelte": "src/index.ts",
@@ -1,9 +1,46 @@
1
+ <!--
2
+ @component
3
+ Renders an image generated by Slop Machine.
4
+
5
+ This component automatically constructs the correct URL for the Slop Machine API,
6
+ displays a loading skeleton or custom loader while fetching, and handles errors.
7
+
8
+ @example
9
+ ```svelte
10
+ <SlopImage
11
+ bucketId="my-bucket"
12
+ resultId="some-result"
13
+ aspectRatio="16:9"
14
+ class="rounded-lg"
15
+ />
16
+ ```
17
+
18
+ @version 0.1.13
19
+ -->
1
20
  <script lang="ts">
2
- import { buildImageUrl, type SlopImageOptions } from "@slopmachine/core";
21
+ import {
22
+ buildImageUrl,
23
+ type SlopImageOptions,
24
+ type ImageAspectRatio,
25
+ } from "@slopmachine/core";
3
26
  import type { Snippet } from "svelte";
4
27
 
5
- interface Props extends SlopImageOptions {
28
+ export interface SlopImageProps extends Omit<
29
+ SlopImageOptions,
30
+ "aspectRatio"
31
+ > {
32
+ /**
33
+ * The aspect ratio of the generated image. Defaults to "1:1".
34
+ */
35
+ aspectRatio?: ImageAspectRatio;
36
+ /**
37
+ * Additional CSS classes to apply to the wrapper element.
38
+ */
6
39
  class?: string;
40
+ /**
41
+ * Custom Svelte snippet to display while the image is loading.
42
+ * If not provided, a default spinner and shimmer effect will be shown.
43
+ */
7
44
  loader?: Snippet;
8
45
  }
9
46
 
@@ -18,7 +55,7 @@
18
55
  class: className = "",
19
56
  loader,
20
57
  ...restProps
21
- }: Props = $props();
58
+ }: SlopImageProps = $props();
22
59
 
23
60
  let isLoading = $state(true);
24
61
 
@@ -96,51 +133,53 @@
96
133
  }
97
134
  </script>
98
135
 
99
- <div
100
- class="slop-wrapper {className}"
101
- style="aspect-ratio: {aspectRatio.replace(':', '/')};"
102
- >
103
- {#if isLoading}
104
- {#if loader}
105
- <div class="custom-loader-wrapper">
106
- {@render loader()}
107
- </div>
108
- {:else}
109
- <div class="loading-overlay">
110
- <div class="spinner-container">
111
- <svg
112
- class="spinner"
113
- xmlns="http://www.w3.org/2000/svg"
114
- fill="none"
115
- viewBox="0 0 24 24"
116
- >
117
- <circle
118
- cx="12"
119
- cy="12"
120
- r="10"
121
- stroke="currentColor"
122
- stroke-width="4"
123
- ></circle>
124
- <path
125
- fill="currentColor"
126
- 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"
127
- ></path>
128
- </svg>
129
- <span>Loading...</span>
136
+ <div class={className}>
137
+ <div
138
+ class="slop-wrapper"
139
+ style="aspect-ratio: {aspectRatio?.replace(':', '/')};"
140
+ >
141
+ {#if isLoading}
142
+ {#if loader}
143
+ <div class="custom-loader-wrapper">
144
+ {@render loader()}
130
145
  </div>
131
- </div>
132
- <div class="shimmer-effect"></div>
146
+ {:else}
147
+ <div class="loading-overlay">
148
+ <div class="spinner-container">
149
+ <svg
150
+ class="spinner"
151
+ xmlns="http://www.w3.org/2000/svg"
152
+ fill="none"
153
+ viewBox="0 0 24 24"
154
+ >
155
+ <circle
156
+ cx="12"
157
+ cy="12"
158
+ r="10"
159
+ stroke="currentColor"
160
+ stroke-width="4"
161
+ ></circle>
162
+ <path
163
+ fill="currentColor"
164
+ 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"
165
+ ></path>
166
+ </svg>
167
+ <span>Loading...</span>
168
+ </div>
169
+ </div>
170
+ <div class="shimmer-effect"></div>
171
+ {/if}
133
172
  {/if}
134
- {/if}
135
-
136
- <img
137
- {src}
138
- {alt}
139
- onload={handleLoad}
140
- onerror={handleError}
141
- class:loaded={!isLoading}
142
- {...restProps}
143
- />
173
+
174
+ <img
175
+ {src}
176
+ {alt}
177
+ onload={handleLoad}
178
+ onerror={handleError}
179
+ class:loaded={!isLoading}
180
+ {...restProps}
181
+ />
182
+ </div>
144
183
  </div>
145
184
 
146
185
  <style>
@@ -0,0 +1,296 @@
1
+ <!--
2
+ @component
3
+ Renders a video generated by Slop Machine.
4
+
5
+ This component automatically constructs the correct URL for the Slop Machine API,
6
+ displays a loading skeleton or custom loader while fetching, and handles errors.
7
+
8
+ @example
9
+ ```svelte
10
+ <SlopVideo
11
+ bucketId="my-bucket"
12
+ resultId="some-result"
13
+ aspectRatio="16:9"
14
+ class="rounded-lg"
15
+ autoplay
16
+ loop
17
+ muted
18
+ />
19
+ ```
20
+ -->
21
+ <script lang="ts">
22
+ import {
23
+ buildVideoUrl,
24
+ type SlopVideoOptions,
25
+ type VideoAspectRatio,
26
+ } from "@slopmachine/core";
27
+ import type { Snippet } from "svelte";
28
+
29
+ export interface SlopVideoProps extends Omit<
30
+ SlopVideoOptions,
31
+ "aspectRatio"
32
+ > {
33
+ /**
34
+ * The aspect ratio of the generated video. Defaults to "16:9".
35
+ */
36
+ aspectRatio?: VideoAspectRatio;
37
+ /**
38
+ * Additional CSS classes to apply to the wrapper element.
39
+ */
40
+ class?: string;
41
+ /**
42
+ * Custom Svelte snippet to display while the video is loading.
43
+ * If not provided, a default spinner and shimmer effect will be shown.
44
+ */
45
+ loader?: Snippet;
46
+ autoplay?: boolean;
47
+ loop?: boolean;
48
+ muted?: boolean;
49
+ playsinline?: boolean;
50
+ [key: string]: any;
51
+ }
52
+
53
+ let {
54
+ bucketId,
55
+ aspectRatio = "16:9",
56
+ version,
57
+ resultId,
58
+ model,
59
+ variables = {},
60
+ duration,
61
+ baseUrl = undefined,
62
+ class: className = "",
63
+ loader,
64
+ autoplay = true,
65
+ loop = true,
66
+ muted = true,
67
+ playsinline = true,
68
+ ...restProps
69
+ }: SlopVideoProps = $props();
70
+
71
+ let isLoading = $state(true);
72
+
73
+ let computedSrc = $derived(
74
+ buildVideoUrl({
75
+ bucketId,
76
+ aspectRatio,
77
+ version,
78
+ resultId,
79
+ variables,
80
+ duration,
81
+ baseUrl,
82
+ model,
83
+ }),
84
+ );
85
+ let src = $state("");
86
+ let prevSrc = $state("");
87
+
88
+ $effect(() => {
89
+ const currentComputedSrc = computedSrc;
90
+ const timeout = setTimeout(() => {
91
+ if (src !== currentComputedSrc) {
92
+ src = currentComputedSrc;
93
+ }
94
+ }, 50);
95
+
96
+ return () => clearTimeout(timeout);
97
+ });
98
+
99
+ $effect(() => {
100
+ if (src !== prevSrc) {
101
+ isLoading = true;
102
+ prevSrc = src;
103
+ }
104
+ });
105
+
106
+ $effect(() => {
107
+ if (src) {
108
+ const abortController = new AbortController();
109
+ fetch(src, { method: "HEAD", signal: abortController.signal })
110
+ .then(async (res) => {
111
+ if (!res.ok) {
112
+ try {
113
+ const errRes = await fetch(src, {
114
+ signal: abortController.signal,
115
+ });
116
+ const errJson = await errRes.json();
117
+ console.error("SlopVideo error:", res.status, errJson.error);
118
+ } catch (e: any) {
119
+ if (e.name !== "AbortError") {
120
+ // Failed to fetch or parse error details
121
+ }
122
+ }
123
+ isLoading = false;
124
+ }
125
+ })
126
+ .catch((err) => {
127
+ if (err.name !== "AbortError") {
128
+ console.error(`Error fetching video from ${src}:`, err);
129
+ isLoading = false;
130
+ }
131
+ });
132
+
133
+ return () => abortController.abort();
134
+ }
135
+ });
136
+
137
+ function handleLoadedData() {
138
+ isLoading = false;
139
+ }
140
+
141
+ function handleError() {
142
+ isLoading = false;
143
+ }
144
+ </script>
145
+
146
+ <div class={className}>
147
+ <div
148
+ class="slop-wrapper"
149
+ style="aspect-ratio: {aspectRatio?.replace(':', '/')};"
150
+ >
151
+ {#if isLoading}
152
+ {#if loader}
153
+ <div class="custom-loader-wrapper">
154
+ {@render loader()}
155
+ </div>
156
+ {:else}
157
+ <div class="loading-overlay">
158
+ <div class="spinner-container">
159
+ <svg
160
+ class="spinner"
161
+ xmlns="http://www.w3.org/2000/svg"
162
+ fill="none"
163
+ viewBox="0 0 24 24"
164
+ >
165
+ <circle
166
+ cx="12"
167
+ cy="12"
168
+ r="10"
169
+ stroke="currentColor"
170
+ stroke-width="4"
171
+ ></circle>
172
+ <path
173
+ fill="currentColor"
174
+ 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"
175
+ ></path>
176
+ </svg>
177
+ <span>Loading...</span>
178
+ </div>
179
+ </div>
180
+ <div class="shimmer-effect"></div>
181
+ {/if}
182
+ {/if}
183
+
184
+ <!-- svelte-ignore a11y_media_has_caption -->
185
+ <video
186
+ {src}
187
+ {autoplay}
188
+ {loop}
189
+ {muted}
190
+ {playsinline}
191
+ onloadeddata={handleLoadedData}
192
+ onerror={handleError}
193
+ class:loaded={!isLoading}
194
+ {...restProps}
195
+ ></video>
196
+ </div>
197
+ </div>
198
+
199
+ <style>
200
+ .slop-wrapper {
201
+ position: relative;
202
+ overflow: hidden;
203
+ background-color: var(--muted, #f3f4f6);
204
+ width: 100%;
205
+ height: 100%;
206
+ }
207
+
208
+ .loading-overlay,
209
+ .custom-loader-wrapper {
210
+ position: absolute;
211
+ top: 0;
212
+ left: 0;
213
+ right: 0;
214
+ bottom: 0;
215
+ z-index: 10;
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ transition: opacity 300ms ease-in-out;
220
+ }
221
+
222
+ .loading-overlay {
223
+ background-color: var(--muted, #f3f4f6);
224
+ }
225
+
226
+ .spinner-container {
227
+ display: flex;
228
+ flex-direction: column;
229
+ align-items: center;
230
+ gap: 0.5rem;
231
+ }
232
+
233
+ .spinner {
234
+ width: 24px;
235
+ height: 24px;
236
+ color: var(--muted-foreground, #6b7280);
237
+ animation: slop-spin 1s linear infinite;
238
+ }
239
+ .spinner circle {
240
+ opacity: 0.25;
241
+ }
242
+ .spinner path {
243
+ opacity: 0.75;
244
+ }
245
+
246
+ .spinner-container span {
247
+ font-size: 0.75rem;
248
+ color: var(--muted-foreground, #6b7280);
249
+ }
250
+
251
+ .shimmer-effect {
252
+ position: absolute;
253
+ top: 0;
254
+ left: 0;
255
+ right: 0;
256
+ bottom: 0;
257
+ background: linear-gradient(
258
+ 90deg,
259
+ var(--muted, #f3f4f6) 0%,
260
+ var(--muted-foreground, #e5e7eb) 50%,
261
+ var(--muted, #f3f4f6) 100%
262
+ );
263
+ background-size: 200% 100%;
264
+ animation: slop-shimmer 2s ease-in-out infinite;
265
+ }
266
+
267
+ video {
268
+ height: 100%;
269
+ width: 100%;
270
+ object-fit: cover;
271
+ transition: opacity 500ms;
272
+ opacity: 0;
273
+ }
274
+
275
+ video.loaded {
276
+ opacity: 1;
277
+ }
278
+
279
+ @keyframes slop-shimmer {
280
+ 0% {
281
+ background-position: 200% 0;
282
+ }
283
+ 100% {
284
+ background-position: -200% 0;
285
+ }
286
+ }
287
+
288
+ @keyframes slop-spin {
289
+ from {
290
+ transform: rotate(0deg);
291
+ }
292
+ to {
293
+ transform: rotate(360deg);
294
+ }
295
+ }
296
+ </style>
package/src/index.ts CHANGED
@@ -1 +1,8 @@
1
- export { default as SlopImage } from "./SlopImage.svelte";
1
+ export { default as SlopImage, type SlopImageProps } from "./SlopImage.svelte";
2
+ export { default as SlopVideo, type SlopVideoProps } from "./SlopVideo.svelte";
3
+ export type {
4
+ ImageAspectRatio,
5
+ VideoAspectRatio,
6
+ SlopImageOptions,
7
+ SlopVideoOptions,
8
+ } from "@slopmachine/core";