@happyvertical/smrt-images 0.30.0

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.
Files changed (75) hide show
  1. package/AGENTS.md +48 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +92 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/categorizer.d.ts +26 -0
  8. package/dist/categorizer.d.ts.map +1 -0
  9. package/dist/deriver.d.ts +33 -0
  10. package/dist/deriver.d.ts.map +1 -0
  11. package/dist/editor.d.ts +72 -0
  12. package/dist/editor.d.ts.map +1 -0
  13. package/dist/image.d.ts +53 -0
  14. package/dist/image.d.ts.map +1 -0
  15. package/dist/images.d.ts +80 -0
  16. package/dist/images.d.ts.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +839 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/manifest.json +1179 -0
  22. package/dist/media-bundle-persistence.d.ts +15 -0
  23. package/dist/media-bundle-persistence.d.ts.map +1 -0
  24. package/dist/metadata.d.ts +19 -0
  25. package/dist/metadata.d.ts.map +1 -0
  26. package/dist/playground.d.ts +2 -0
  27. package/dist/playground.d.ts.map +1 -0
  28. package/dist/playground.js +140 -0
  29. package/dist/playground.js.map +1 -0
  30. package/dist/prompts.d.ts +8 -0
  31. package/dist/prompts.d.ts.map +1 -0
  32. package/dist/search.d.ts +42 -0
  33. package/dist/search.d.ts.map +1 -0
  34. package/dist/smrt-knowledge.json +561 -0
  35. package/dist/svelte/components/AssetsGallery.svelte +436 -0
  36. package/dist/svelte/components/AssetsGallery.svelte.d.ts +11 -0
  37. package/dist/svelte/components/AssetsGallery.svelte.d.ts.map +1 -0
  38. package/dist/svelte/components/ImageEditor.svelte +485 -0
  39. package/dist/svelte/components/ImageEditor.svelte.d.ts +12 -0
  40. package/dist/svelte/components/ImageEditor.svelte.d.ts.map +1 -0
  41. package/dist/svelte/components/ImageUploader.svelte +922 -0
  42. package/dist/svelte/components/ImageUploader.svelte.d.ts +15 -0
  43. package/dist/svelte/components/ImageUploader.svelte.d.ts.map +1 -0
  44. package/dist/svelte/i18n.d.ts +42 -0
  45. package/dist/svelte/i18n.d.ts.map +1 -0
  46. package/dist/svelte/i18n.js +46 -0
  47. package/dist/svelte/image-clients.d.ts +45 -0
  48. package/dist/svelte/image-clients.d.ts.map +1 -0
  49. package/dist/svelte/image-clients.js +1 -0
  50. package/dist/svelte/index.d.ts +14 -0
  51. package/dist/svelte/index.d.ts.map +1 -0
  52. package/dist/svelte/index.js +21 -0
  53. package/dist/svelte/playground.d.ts +74 -0
  54. package/dist/svelte/playground.d.ts.map +1 -0
  55. package/dist/svelte/playground.js +105 -0
  56. package/dist/svelte/routes/ImageStudioRoute.svelte +194 -0
  57. package/dist/svelte/routes/ImageStudioRoute.svelte.d.ts +7 -0
  58. package/dist/svelte/routes/ImageStudioRoute.svelte.d.ts.map +1 -0
  59. package/dist/svelte/routes/index.d.ts +2 -0
  60. package/dist/svelte/routes/index.d.ts.map +1 -0
  61. package/dist/svelte/routes/index.js +1 -0
  62. package/dist/svelte/routes/shared.d.ts +25 -0
  63. package/dist/svelte/routes/shared.d.ts.map +1 -0
  64. package/dist/svelte/routes/shared.js +31 -0
  65. package/dist/types.d.ts +51 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +2 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/ui.d.ts +10 -0
  70. package/dist/ui.d.ts.map +1 -0
  71. package/dist/ui.js +42 -0
  72. package/dist/ui.js.map +1 -0
  73. package/dist/upstream.d.ts +65 -0
  74. package/dist/upstream.d.ts.map +1 -0
  75. package/package.json +95 -0
@@ -0,0 +1,436 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
+ import { onMount } from 'svelte';
4
+ import { M } from '../i18n.js';
5
+ import type {
6
+ ImageLike,
7
+ ImagesGalleryClient,
8
+ ImagesGalleryQuery,
9
+ } from '../image-clients';
10
+
11
+ const { t } = useI18n();
12
+
13
+ let {
14
+ apiBaseUrl = '/api/v1',
15
+ client = undefined,
16
+ onSelect = undefined,
17
+ enableDragToEditor = false,
18
+ }: {
19
+ apiBaseUrl?: string;
20
+ client?: ImagesGalleryClient;
21
+ onSelect?: (image: ImageLike) => void;
22
+ enableDragToEditor?: boolean;
23
+ } = $props();
24
+
25
+ let images: ImageLike[] = $state([]);
26
+ let isLoading = $state(false);
27
+ let error: string | null = $state(null);
28
+
29
+ // Filters
30
+ let searchQuery = $state('');
31
+ let orientationFilter: 'all' | 'landscape' | 'portrait' | 'square' =
32
+ $state('all');
33
+ let minWidth = $state('');
34
+ let minHeight = $state('');
35
+
36
+ // Pagination (simple implementation for now)
37
+ let limit = 24;
38
+ let offset = $state(0);
39
+ let hasMore = $state(true);
40
+
41
+ async function loadImagesFromApi(query: ImagesGalleryQuery) {
42
+ const params = new URLSearchParams({
43
+ limit: query.limit.toString(),
44
+ offset: query.offset.toString(),
45
+ });
46
+
47
+ if (query.q) params.append('q', query.q);
48
+ if (query.orientation) params.append('orientation', query.orientation);
49
+ if (typeof query.minWidth === 'number') {
50
+ params.append('minWidth', query.minWidth.toString());
51
+ }
52
+ if (typeof query.minHeight === 'number') {
53
+ params.append('minHeight', query.minHeight.toString());
54
+ }
55
+
56
+ const res = await fetch(`${apiBaseUrl}/images?${params.toString()}`);
57
+ if (!res.ok) {
58
+ let errText = await res.text();
59
+ if (errText.trim().startsWith('<'))
60
+ errText = `Server returned ${res.status} ${res.statusText}`;
61
+ throw new Error(errText);
62
+ }
63
+
64
+ return (await res.json()) as { items: ImageLike[] };
65
+ }
66
+
67
+ async function loadImages(reset = false) {
68
+ if (isLoading) return;
69
+
70
+ if (reset) {
71
+ images = [];
72
+ offset = 0;
73
+ hasMore = true;
74
+ }
75
+
76
+ if (!hasMore && !reset) return;
77
+
78
+ isLoading = true;
79
+ error = null;
80
+
81
+ try {
82
+ const query: ImagesGalleryQuery = {
83
+ limit,
84
+ offset,
85
+ ...(searchQuery ? { q: searchQuery } : {}),
86
+ ...(orientationFilter !== 'all'
87
+ ? { orientation: orientationFilter }
88
+ : {}),
89
+ ...(minWidth ? { minWidth: Number(minWidth) } : {}),
90
+ ...(minHeight ? { minHeight: Number(minHeight) } : {}),
91
+ };
92
+ const data = client
93
+ ? await client.list(query)
94
+ : await loadImagesFromApi(query);
95
+
96
+ if (data.items.length < limit) {
97
+ hasMore = false;
98
+ }
99
+
100
+ images = [...images, ...data.items];
101
+ offset += data.items.length;
102
+ } catch (e: any) {
103
+ error = e.message || 'Failed to load images';
104
+ } finally {
105
+ isLoading = false;
106
+ }
107
+ }
108
+
109
+ // Initial load — must be in onMount to avoid SSR fetch errors
110
+ onMount(() => loadImages(true));
111
+
112
+ // Debounce search on filter changes using idiomatic Svelte 5 $effect cleanup
113
+ let searchTimeout: ReturnType<typeof setTimeout> | undefined;
114
+ $effect(() => {
115
+ // Register reactive deps
116
+ const _q = searchQuery;
117
+ const _o = orientationFilter;
118
+ const _w = minWidth;
119
+ const _h = minHeight;
120
+ return () => {
121
+ clearTimeout(searchTimeout);
122
+ searchTimeout = setTimeout(() => loadImages(true), 500);
123
+ };
124
+ });
125
+
126
+ function handleSelect(image: ImageLike) {
127
+ if (onSelect) {
128
+ onSelect(image);
129
+ }
130
+ }
131
+
132
+ function handleDragStart(event: DragEvent, image: ImageLike) {
133
+ if (!enableDragToEditor || !event.dataTransfer) {
134
+ return;
135
+ }
136
+
137
+ const source = image.sourceUri || image.url || '';
138
+ event.dataTransfer.effectAllowed = 'copy';
139
+ event.dataTransfer.setData('application/x-smrt-image', JSON.stringify(image));
140
+ if (source) {
141
+ event.dataTransfer.setData('text/uri-list', source);
142
+ event.dataTransfer.setData('text/plain', source);
143
+ }
144
+ }
145
+ </script>
146
+
147
+ <div class="smrt-assets-gallery">
148
+ <div class="gallery-header">
149
+ <h3>{t(M['images.assets_gallery.title'])}</h3>
150
+
151
+ <div class="toolbar">
152
+ <div class="search-box">
153
+ <input
154
+ type="search"
155
+ bind:value={searchQuery}
156
+ placeholder={t(M['images.assets_gallery.search_placeholder'])}
157
+ />
158
+ </div>
159
+
160
+ <div class="filters">
161
+ <select bind:value={orientationFilter}>
162
+ <option value="all">{t(M['images.assets_gallery.any_orientation'])}</option>
163
+ <option value="landscape">Landscape</option>
164
+ <option value="portrait">Portrait</option>
165
+ <option value="square">Square</option>
166
+ </select>
167
+
168
+ <input
169
+ type="number"
170
+ bind:value={minWidth}
171
+ placeholder={t(M['images.assets_gallery.min_width_placeholder'])}
172
+ class="size-input"
173
+ />
174
+ <input
175
+ type="number"
176
+ bind:value={minHeight}
177
+ placeholder={t(M['images.assets_gallery.min_height_placeholder'])}
178
+ class="size-input"
179
+ />
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ {#if error}
185
+ <div class="error-msg">{error}</div>
186
+ {/if}
187
+
188
+ <div class="gallery-grid">
189
+ {#each images as image (image.id)}
190
+ {#if onSelect}
191
+ <button
192
+ type="button"
193
+ class="gallery-item"
194
+ class:selectable={true}
195
+ draggable={enableDragToEditor}
196
+ ondragstart={(event) => handleDragStart(event, image)}
197
+ onclick={() => handleSelect(image)}
198
+ >
199
+ <div class="img-container">
200
+ <img src={image.sourceUri || image.url} alt={image.alt || image.name} loading="lazy" />
201
+ </div>
202
+ <div class="item-info">
203
+ <span class="item-name" title={image.name}>{image.name}</span>
204
+ <span class="item-meta">{image.width}x{image.height} • {image.mimeType}</span>
205
+ </div>
206
+ </button>
207
+ {:else}
208
+ <div class="gallery-item">
209
+ <div class="img-container">
210
+ <img src={image.sourceUri || image.url} alt={image.alt || image.name} loading="lazy" />
211
+ </div>
212
+ <div class="item-info">
213
+ <span class="item-name" title={image.name}>{image.name}</span>
214
+ <span class="item-meta">{image.width}x{image.height} • {image.mimeType}</span>
215
+ </div>
216
+ </div>
217
+ {/if}
218
+ {/each}
219
+
220
+ {#if images.length === 0 && !isLoading}
221
+ <div class="empty-state">
222
+ <p>{t(M['images.assets_gallery.no_images_found'])}</p>
223
+ </div>
224
+ {/if}
225
+ </div>
226
+
227
+ {#if hasMore}
228
+ <div class="load-more">
229
+ <button
230
+ onclick={() => loadImages(false)}
231
+ disabled={isLoading}
232
+ class="load-more-btn"
233
+ >
234
+ {isLoading ? 'Loading...' : 'Load More'}
235
+ </button>
236
+ </div>
237
+ {/if}
238
+ </div>
239
+
240
+ <style>
241
+ .smrt-assets-gallery {
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: 1.5rem;
245
+ height: 100%;
246
+ min-height: 400px;
247
+ background: var(--smrt-color-surface, #121212);
248
+ color: var(--smrt-color-on-surface, #fff);
249
+ }
250
+
251
+ .gallery-header {
252
+ display: flex;
253
+ flex-direction: column;
254
+ gap: 1rem;
255
+ padding: 1rem;
256
+ background: var(--smrt-color-surface-container, #1a1a1a);
257
+ border-radius: var(--smrt-radius-lg, 8px);
258
+ border: 1px solid var(--smrt-color-outline-variant, #333);
259
+ }
260
+
261
+ .gallery-header h3 {
262
+ margin: 0;
263
+ font-size: var(--smrt-typography-title-large-size, 1.25rem);
264
+ }
265
+
266
+ .toolbar {
267
+ display: flex;
268
+ flex-wrap: wrap;
269
+ gap: 1rem;
270
+ align-items: center;
271
+ justify-content: space-between;
272
+ }
273
+
274
+ .search-box {
275
+ flex: 1;
276
+ min-width: 250px;
277
+ }
278
+
279
+ .search-box input {
280
+ width: 100%;
281
+ padding: 0.75rem 1.25rem;
282
+ background: var(--smrt-color-surface-container-highest, #333);
283
+ border: 1px solid var(--smrt-color-outline-variant, #444);
284
+ border-radius: var(--smrt-radius-full, 9999px);
285
+ color: inherit;
286
+ font-size: var(--smrt-typography-body-large-size, 0.95rem);
287
+ transition: box-shadow 0.2s, border-color 0.2s;
288
+ }
289
+
290
+ .search-box input:focus {
291
+ outline: none;
292
+ border-color: var(--smrt-color-primary, #3b82f6);
293
+ box-shadow: inset 0 0 0 1px var(--smrt-color-primary, #3b82f6);
294
+ }
295
+
296
+ .filters {
297
+ display: flex;
298
+ gap: 0.75rem;
299
+ flex-wrap: wrap;
300
+ }
301
+
302
+ .filters select, .filters input {
303
+ padding: 0.75rem 1rem;
304
+ background: var(--smrt-color-surface-container-high, #242424);
305
+ border: 1px solid var(--smrt-color-outline-variant, #444);
306
+ border-radius: var(--smrt-radius-sm, 4px);
307
+ color: inherit;
308
+ transition: box-shadow 0.2s, border-color 0.2s;
309
+ }
310
+
311
+ .filters select:focus, .filters input:focus {
312
+ outline: none;
313
+ border-color: var(--smrt-color-primary, #3b82f6);
314
+ box-shadow: inset 0 0 0 1px var(--smrt-color-primary, #3b82f6);
315
+ }
316
+
317
+ .size-input {
318
+ width: 120px;
319
+ }
320
+
321
+ .error-msg {
322
+ color: var(--smrt-color-error, #ef4444);
323
+ background: color-mix(in srgb, var(--smrt-color-error) 10%, transparent);
324
+ padding: 1rem;
325
+ border-radius: var(--smrt-radius-md, 8px);
326
+ }
327
+
328
+ .gallery-grid {
329
+ display: grid;
330
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
331
+ gap: 1rem;
332
+ overflow-y: auto;
333
+ padding: 0.25rem;
334
+ }
335
+
336
+ .gallery-item {
337
+ display: flex;
338
+ flex-direction: column;
339
+ align-items: stretch;
340
+ padding: 0;
341
+ background: var(--smrt-color-surface-container, #1a1a1a);
342
+ border-radius: var(--smrt-radius-lg, 8px);
343
+ border: 1px solid var(--smrt-color-surface-container-high, #2a2a2a);
344
+ box-shadow: var(--smrt-elevation-1, 0 1px 3px color-mix(in srgb, var(--smrt-color-shadow) 12%, transparent), 0 1px 2px color-mix(in srgb, var(--smrt-color-shadow) 24%, transparent));
345
+ overflow: hidden;
346
+ color: inherit;
347
+ text-align: left;
348
+ transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
349
+ }
350
+
351
+ .gallery-item.selectable {
352
+ cursor: pointer;
353
+ }
354
+
355
+ .gallery-item.selectable:hover, .gallery-item.selectable:focus {
356
+ transform: translateY(-2px);
357
+ border-color: var(--smrt-color-primary, #3b82f6);
358
+ box-shadow: var(--smrt-elevation-2, 0 3px 6px color-mix(in srgb, var(--smrt-color-shadow) 16%, transparent), 0 3px 6px color-mix(in srgb, var(--smrt-color-shadow) 23%, transparent));
359
+ outline: none;
360
+ }
361
+
362
+ .img-container {
363
+ aspect-ratio: 1;
364
+ width: 100%;
365
+ background: var(--smrt-color-surface-container-high, #242424);
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ overflow: hidden;
370
+ }
371
+
372
+ .img-container img {
373
+ max-width: 100%;
374
+ max-height: 100%;
375
+ object-fit: cover;
376
+ width: 100%;
377
+ height: 100%;
378
+ }
379
+
380
+ .item-info {
381
+ padding: 1rem;
382
+ display: flex;
383
+ flex-direction: column;
384
+ gap: 0.35rem;
385
+ }
386
+
387
+ .item-name {
388
+ font-weight: var(--smrt-typography-weight-medium, 500);
389
+ font-size: var(--smrt-typography-title-medium-size, 1rem);
390
+ white-space: nowrap;
391
+ overflow: hidden;
392
+ text-overflow: ellipsis;
393
+ letter-spacing: var(--smrt-typography-title-medium-tracking, 0.1px);
394
+ }
395
+
396
+ .item-meta {
397
+ font-size: var(--smrt-typography-label-large-size, 0.85rem);
398
+ color: var(--smrt-color-outline, #888);
399
+ }
400
+
401
+ .empty-state {
402
+ grid-column: 1 / -1;
403
+ text-align: center;
404
+ padding: 3rem;
405
+ color: var(--smrt-color-outline, #888);
406
+ background: var(--smrt-color-surface-container, #1a1a1a);
407
+ border-radius: var(--smrt-radius-lg, 8px);
408
+ border: 1px dashed var(--smrt-color-outline-variant, #444);
409
+ }
410
+
411
+ .load-more {
412
+ display: flex;
413
+ justify-content: center;
414
+ padding: 1rem 0;
415
+ }
416
+
417
+ .load-more-btn {
418
+ padding: 0.75rem 2rem;
419
+ background: var(--smrt-color-surface-container-high, #242424);
420
+ color: var(--smrt-color-primary, #3b82f6);
421
+ border: 1px solid var(--smrt-color-outline-variant, #444);
422
+ border-radius: var(--smrt-radius-full, 9999px);
423
+ font-weight: var(--smrt-typography-weight-medium, 500);
424
+ cursor: pointer;
425
+ transition: background 0.2s;
426
+ }
427
+
428
+ .load-more-btn:hover:not(:disabled) {
429
+ background: var(--smrt-color-surface-container-highest, #333);
430
+ }
431
+
432
+ .load-more-btn:disabled {
433
+ opacity: 0.6;
434
+ cursor: not-allowed;
435
+ }
436
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { ImageLike, ImagesGalleryClient } from '../image-clients';
2
+ type $$ComponentProps = {
3
+ apiBaseUrl?: string;
4
+ client?: ImagesGalleryClient;
5
+ onSelect?: (image: ImageLike) => void;
6
+ enableDragToEditor?: boolean;
7
+ };
8
+ declare const AssetsGallery: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type AssetsGallery = ReturnType<typeof AssetsGallery>;
10
+ export default AssetsGallery;
11
+ //# sourceMappingURL=AssetsGallery.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssetsGallery.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssetsGallery.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,SAAS,EACT,mBAAmB,EAEpB,MAAM,kBAAkB,CAAC;AAEzB,KAAK,gBAAgB,GAAI;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAkNF,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}