@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,485 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
+ import { M } from '../i18n.js';
4
+ import type {
5
+ ImageConvertRequest,
6
+ ImageCropRequest,
7
+ ImageEditorClient,
8
+ ImageLike,
9
+ ImageResizeRequest,
10
+ } from '../image-clients';
11
+
12
+ const { t } = useI18n();
13
+
14
+ let {
15
+ image = null,
16
+ apiBaseUrl = '/api/v1',
17
+ client = undefined,
18
+ onSave = undefined,
19
+ onCancel = undefined,
20
+ }: {
21
+ image?: ImageLike | null;
22
+ apiBaseUrl?: string;
23
+ client?: ImageEditorClient;
24
+ onSave?: (image: ImageLike) => void;
25
+ onCancel?: () => void;
26
+ } = $props();
27
+
28
+ let mode: 'standard' | 'ai' = $state('standard');
29
+
30
+ let isEditingDimensions = $state(false);
31
+ let isCropping = $state(false);
32
+
33
+ // Set default state empty and populate through effect
34
+ let width = $state(800);
35
+ let height = $state(600);
36
+ let cropX = $state(0);
37
+ let cropY = $state(0);
38
+ let cropW = $state(800);
39
+ let cropH = $state(600);
40
+ let format = $state('webp');
41
+
42
+ // Track if we've initialized dimensions for the *current* image
43
+ let lastImageId = $state<string | undefined>(undefined);
44
+
45
+ // AI Mode options
46
+ let prompt = $state('');
47
+
48
+ let isProcessing = $state(false);
49
+ let error: string | null = $state(null);
50
+ let successMessage: string | null = $state(null);
51
+
52
+ async function postEditorRequest<TPayload extends object>(
53
+ endpoint: string,
54
+ payload: TPayload,
55
+ ) {
56
+ const res = await fetch(`${apiBaseUrl}/images/${image?.id}/${endpoint}`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify(payload),
60
+ });
61
+
62
+ if (!res.ok) {
63
+ let errText = await res.text();
64
+ if (errText.trim().startsWith('<'))
65
+ errText = `Server returned ${res.status} ${res.statusText}`;
66
+ throw new Error(errText);
67
+ }
68
+
69
+ return (await res.json()) as { image: ImageLike };
70
+ }
71
+
72
+ $effect(() => {
73
+ // Only reset dimensions if the actual image selection changes
74
+ if (image?.id && image.id !== lastImageId) {
75
+ lastImageId = image.id;
76
+ resetDimensions();
77
+ }
78
+ });
79
+
80
+ function resetDimensions() {
81
+ if (!image) return;
82
+ width = image.width;
83
+ height = image.height;
84
+ cropW = image.width;
85
+ cropH = image.height;
86
+ isEditingDimensions = false;
87
+ isCropping = false;
88
+ }
89
+
90
+ async function handleResize() {
91
+ if (!image) return;
92
+ isProcessing = true;
93
+ error = null;
94
+ successMessage = null;
95
+
96
+ try {
97
+ const payload: ImageResizeRequest = { width, height };
98
+ const data = client
99
+ ? await client.resize(image.id, payload)
100
+ : await postEditorRequest('resize', payload);
101
+
102
+ successMessage = 'Image resized successfully (new derivative created).';
103
+ if (onSave) onSave(data.image);
104
+ } catch (e: any) {
105
+ error = e.message || 'Failed to resize image';
106
+ } finally {
107
+ isProcessing = false;
108
+ }
109
+ }
110
+
111
+ async function handleCrop() {
112
+ if (!image) return;
113
+ isProcessing = true;
114
+ error = null;
115
+ successMessage = null;
116
+
117
+ try {
118
+ const payload: ImageCropRequest = {
119
+ x: cropX,
120
+ y: cropY,
121
+ w: cropW,
122
+ h: cropH,
123
+ };
124
+ const data = client
125
+ ? await client.crop(image.id, payload)
126
+ : await postEditorRequest('crop', payload);
127
+
128
+ successMessage = 'Image cropped successfully (new derivative created).';
129
+ if (onSave) onSave(data.image);
130
+ } catch (e: any) {
131
+ error = e.message || 'Failed to crop image';
132
+ } finally {
133
+ isProcessing = false;
134
+ }
135
+ }
136
+
137
+ async function handleConvert() {
138
+ if (!image) return;
139
+ isProcessing = true;
140
+ error = null;
141
+ successMessage = null;
142
+
143
+ try {
144
+ const payload: ImageConvertRequest = { format };
145
+ const data = client
146
+ ? await client.convert(image.id, payload)
147
+ : await postEditorRequest('convert', payload);
148
+
149
+ successMessage = `Image converted to ${format} successfully (new derivative created).`;
150
+ if (onSave) onSave(data.image);
151
+ } catch (e: any) {
152
+ error = e.message || 'Failed to convert image';
153
+ } finally {
154
+ isProcessing = false;
155
+ }
156
+ }
157
+
158
+ async function handleAIEdit() {
159
+ if (!image || !prompt) return;
160
+ isProcessing = true;
161
+ error = null;
162
+ successMessage = null;
163
+
164
+ try {
165
+ const data = client
166
+ ? await client.edit(image.id, { prompt })
167
+ : await postEditorRequest('edit', { prompt });
168
+
169
+ successMessage = 'AI edit complete (new derivative created).';
170
+ if (onSave) onSave(data.image);
171
+ } catch (e: any) {
172
+ error = e.message || 'Failed to apply AI edit';
173
+ } finally {
174
+ isProcessing = false;
175
+ }
176
+ }
177
+ </script>
178
+
179
+ <div class="smrt-image-editor">
180
+ <div class="header">
181
+ <h3>{t(M['images.image_editor.title'])}</h3>
182
+ {#if onCancel}
183
+ <button class="close-btn" onclick={onCancel}>×</button>
184
+ {/if}
185
+ </div>
186
+
187
+ {#if !image}
188
+ <div class="empty-state">{t(M['images.image_editor.no_image_selected'])}</div>
189
+ {:else}
190
+ <div class="editor-content">
191
+ <div class="image-preview" style="background-image: url({image.url})">
192
+ <!-- Optional: Interactive crop overlay could go here -->
193
+ </div>
194
+
195
+ <div class="editor-controls">
196
+ <div class="mode-selector">
197
+ <button class:active={mode === 'standard'} onclick={() => mode = 'standard'}>{t(M['images.image_editor.standard_tools'])}</button>
198
+ <button class:active={mode === 'ai'} onclick={() => mode = 'ai'}>{t(M['images.image_editor.ai_edit'])}</button>
199
+ </div>
200
+
201
+ {#if error}
202
+ <div class="error-msg">{error}</div>
203
+ {/if}
204
+ {#if successMessage}
205
+ <div class="success-msg">{successMessage}</div>
206
+ {/if}
207
+
208
+ {#if mode === 'standard'}
209
+ <!-- Standard Operations -->
210
+ <div class="tool-section">
211
+ <h4>Resize</h4>
212
+ <div class="row">
213
+ <label>Width <input type="number" bind:value={width} onfocus={() => isEditingDimensions = true} /></label>
214
+ <label>Height <input type="number" bind:value={height} onfocus={() => isEditingDimensions = true} /></label>
215
+ <button disabled={isProcessing} onclick={handleResize} class="tonal-btn">{t(M['images.image_editor.apply_resize'])}</button>
216
+ </div>
217
+ {#if isEditingDimensions}
218
+ <div class="row"><button class="text-btn hint" onclick={resetDimensions}>{t(M['images.image_editor.reset_dimensions'])}</button></div>
219
+ {/if}
220
+ </div>
221
+
222
+ <div class="tool-section">
223
+ <h4>Crop</h4>
224
+ <div class="row">
225
+ <label>X <input type="number" bind:value={cropX} onfocus={() => isCropping = true} /></label>
226
+ <label>Y <input type="number" bind:value={cropY} onfocus={() => isCropping = true} /></label>
227
+ </div>
228
+ <div class="row">
229
+ <label>W <input type="number" bind:value={cropW} onfocus={() => isCropping = true} /></label>
230
+ <label>H <input type="number" bind:value={cropH} onfocus={() => isCropping = true} /></label>
231
+ <button disabled={isProcessing} onclick={handleCrop} class="tonal-btn">{t(M['images.image_editor.apply_crop'])}</button>
232
+ </div>
233
+ {#if isCropping}
234
+ <div class="row"><button class="text-btn hint" onclick={resetDimensions}>{t(M['images.image_editor.reset_dimensions'])}</button></div>
235
+ {/if}
236
+ </div>
237
+
238
+ <div class="tool-section">
239
+ <h4>{t(M['images.image_editor.convert_format'])}</h4>
240
+ <div class="row">
241
+ <select bind:value={format}>
242
+ <option value="webp">WebP</option>
243
+ <option value="jpeg">JPEG</option>
244
+ <option value="png">PNG</option>
245
+ </select>
246
+ <button disabled={isProcessing} onclick={handleConvert} class="tonal-btn">Convert</button>
247
+ </div>
248
+ </div>
249
+ {:else}
250
+ <!-- AI Operations -->
251
+ <div class="tool-section">
252
+ <h4>{t(M['images.image_editor.ai_powered_edit'])}</h4>
253
+ <p class="hint">{t(M['images.image_editor.ai_powered_edit_hint'])}</p>
254
+ <textarea
255
+ bind:value={prompt}
256
+ placeholder={t(M['images.image_editor.ai_prompt_placeholder'])}
257
+ rows="4"
258
+ ></textarea>
259
+ <button
260
+ disabled={isProcessing || !prompt.trim()}
261
+ onclick={handleAIEdit}
262
+ class="primary-btn"
263
+ >
264
+ {isProcessing ? 'Generating...' : 'Apply AI Edit'}
265
+ </button>
266
+ </div>
267
+ {/if}
268
+ </div>
269
+ </div>
270
+ {/if}
271
+ </div>
272
+
273
+ <style>
274
+ .smrt-image-editor {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 1rem;
278
+ padding: 1.5rem;
279
+ background: var(--smrt-color-surface-container, #1a1a1a);
280
+ color: var(--smrt-color-on-surface, #fff);
281
+ border-radius: var(--smrt-radius-lg, 8px);
282
+ border: 1px solid var(--smrt-color-outline-variant, #333);
283
+ }
284
+
285
+ .header {
286
+ display: flex;
287
+ justify-content: space-between;
288
+ align-items: center;
289
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #333);
290
+ padding-bottom: 1rem;
291
+ margin-bottom: 0.5rem;
292
+ }
293
+
294
+ .header h3 {
295
+ margin: 0;
296
+ font-size: var(--smrt-typography-title-medium-size, 1.15rem);
297
+ font-weight: var(--smrt-typography-weight-medium, 500);
298
+ }
299
+
300
+ .close-btn {
301
+ background: transparent;
302
+ border: none;
303
+ color: inherit;
304
+ font-size: var(--smrt-typography-headline-small-size, 1.5rem);
305
+ cursor: pointer;
306
+ }
307
+
308
+ .empty-state {
309
+ padding: 2rem;
310
+ text-align: center;
311
+ color: var(--smrt-color-outline, #666);
312
+ }
313
+
314
+ .editor-content {
315
+ display: flex;
316
+ flex-wrap: wrap;
317
+ gap: 2rem;
318
+ }
319
+
320
+ .image-preview {
321
+ flex: 1;
322
+ min-width: 300px;
323
+ min-height: 300px;
324
+ background-size: contain;
325
+ background-position: center;
326
+ background-repeat: no-repeat;
327
+ background-color: var(--smrt-color-surface-container-high, #242424);
328
+ border-radius: var(--smrt-radius-md, 6px);
329
+ border: 1px dashed var(--smrt-color-outline-variant, #444);
330
+ }
331
+
332
+ .editor-controls {
333
+ flex: 1;
334
+ min-width: 300px;
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 1.5rem;
338
+ }
339
+
340
+ /* Segmented button style for tabs */
341
+ .mode-selector {
342
+ display: flex;
343
+ background: var(--smrt-color-surface-container-high, #242424);
344
+ border-radius: var(--smrt-radius-full, 9999px);
345
+ padding: 0.25rem;
346
+ gap: 0.25rem;
347
+ }
348
+
349
+ .mode-selector button {
350
+ flex: 1;
351
+ background: transparent;
352
+ border: none;
353
+ padding: 0.6rem 1rem;
354
+ color: var(--smrt-color-outline, #666);
355
+ cursor: pointer;
356
+ font-weight: var(--smrt-typography-weight-medium, 500);
357
+ border-radius: var(--smrt-radius-full, 9999px);
358
+ transition: all 0.2s;
359
+ }
360
+
361
+ .mode-selector button.active {
362
+ background: var(--smrt-color-surface-container, #1a1a1a);
363
+ color: var(--smrt-color-on-surface, #fff);
364
+ box-shadow: var(--smrt-elevation-1, 0 1px 3px color-mix(in srgb, var(--smrt-color-shadow) 20%, transparent));
365
+ }
366
+
367
+ .tool-section h4 {
368
+ margin: 0 0 1rem 0;
369
+ font-size: var(--smrt-typography-title-medium-size, 0.95rem);
370
+ font-weight: var(--smrt-typography-weight-medium, 500);
371
+ color: var(--smrt-color-on-surface-variant, #ccc);
372
+ }
373
+
374
+ .row {
375
+ display: flex;
376
+ gap: 0.75rem;
377
+ align-items: center;
378
+ margin-bottom: 0.75rem;
379
+ flex-wrap: wrap;
380
+ }
381
+
382
+ .row label {
383
+ display: flex;
384
+ flex-direction: column;
385
+ font-size: var(--smrt-typography-label-medium-size, 0.8rem);
386
+ font-weight: var(--smrt-typography-weight-medium, 500);
387
+ color: var(--smrt-color-outline, #888);
388
+ gap: 0.25rem;
389
+ }
390
+
391
+ input, select, textarea {
392
+ background: var(--smrt-color-surface-container-high, #242424);
393
+ border: 1px solid var(--smrt-color-outline-variant, #444);
394
+ color: inherit;
395
+ padding: 0.6rem 0.75rem;
396
+ border-radius: var(--smrt-radius-sm, 4px);
397
+ transition: box-shadow 0.2s, border-color 0.2s;
398
+ }
399
+
400
+ input:focus, select:focus, textarea:focus {
401
+ outline: none;
402
+ border-color: var(--smrt-color-primary, #3b82f6);
403
+ box-shadow: inset 0 0 0 1px var(--smrt-color-primary, #3b82f6);
404
+ }
405
+
406
+ input[type="number"] {
407
+ width: 90px;
408
+ }
409
+
410
+ textarea {
411
+ width: 100%;
412
+ resize: vertical;
413
+ margin-bottom: 1rem;
414
+ font-family: inherit;
415
+ }
416
+
417
+ .tonal-btn {
418
+ background: var(--smrt-color-surface-container-highest, #333);
419
+ color: var(--smrt-color-primary, #3b82f6);
420
+ border: 1px solid var(--smrt-color-outline-variant, #444);
421
+ padding: 0.6rem 1.2rem;
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
+ margin-top: 1.25rem;
427
+ }
428
+
429
+ .tonal-btn:hover:not(:disabled) {
430
+ background: var(--smrt-color-surface-container-high, #3f3f3f);
431
+ }
432
+
433
+ .text-btn {
434
+ background: transparent;
435
+ border: none;
436
+ cursor: pointer;
437
+ text-decoration: underline;
438
+ padding: 0;
439
+ }
440
+ .text-btn:hover {
441
+ color: var(--smrt-color-on-surface, #fff);
442
+ }
443
+
444
+ .primary-btn {
445
+ background: var(--smrt-color-primary, #3b82f6);
446
+ color: white;
447
+ border: none;
448
+ padding: 0.6rem 1.5rem;
449
+ border-radius: var(--smrt-radius-full, 9999px);
450
+ font-weight: var(--smrt-typography-weight-medium, 500);
451
+ cursor: pointer;
452
+ transition: background 0.2s, opacity 0.2s;
453
+ }
454
+
455
+ .primary-btn:hover:not(:disabled) {
456
+ filter: brightness(1.1);
457
+ }
458
+
459
+ button:disabled {
460
+ opacity: 0.5;
461
+ cursor: not-allowed;
462
+ }
463
+
464
+ .hint {
465
+ font-size: var(--smrt-typography-body-small-size, 0.8rem);
466
+ color: var(--smrt-color-outline, #888);
467
+ margin: 0 0 0.5rem 0;
468
+ }
469
+
470
+ .error-msg {
471
+ color: var(--smrt-color-error, #ef4444);
472
+ background: color-mix(in srgb, var(--smrt-color-error) 10%, transparent);
473
+ padding: 0.5rem;
474
+ border-radius: var(--smrt-radius-sm, 4px);
475
+ font-size: var(--smrt-typography-body-medium-size, 0.85rem);
476
+ }
477
+
478
+ .success-msg {
479
+ color: var(--smrt-color-success, #22c55e);
480
+ background: color-mix(in srgb, var(--smrt-color-success) 10%, transparent);
481
+ padding: 0.5rem;
482
+ border-radius: var(--smrt-radius-sm, 4px);
483
+ font-size: var(--smrt-typography-body-medium-size, 0.85rem);
484
+ }
485
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { ImageEditorClient, ImageLike } from '../image-clients';
2
+ type $$ComponentProps = {
3
+ image?: ImageLike | null;
4
+ apiBaseUrl?: string;
5
+ client?: ImageEditorClient;
6
+ onSave?: (image: ImageLike) => void;
7
+ onCancel?: () => void;
8
+ };
9
+ declare const ImageEditor: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type ImageEditor = ReturnType<typeof ImageEditor>;
11
+ export default ImageEditor;
12
+ //# sourceMappingURL=ImageEditor.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ImageEditor.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,iBAAiB,EACjB,SAAS,EAEV,MAAM,kBAAkB,CAAC;AAEzB,KAAK,gBAAgB,GAAI;IACxB,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAiQF,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}