@happyvertical/smrt-assets 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 (119) hide show
  1. package/AGENTS.md +78 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +136 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/asset-association.d.ts +16 -0
  8. package/dist/asset-association.d.ts.map +1 -0
  9. package/dist/asset-associations.d.ts +27 -0
  10. package/dist/asset-associations.d.ts.map +1 -0
  11. package/dist/asset-capabilities.d.ts +137 -0
  12. package/dist/asset-capabilities.d.ts.map +1 -0
  13. package/dist/asset-conventions.d.ts +76 -0
  14. package/dist/asset-conventions.d.ts.map +1 -0
  15. package/dist/asset-metafield.d.ts +27 -0
  16. package/dist/asset-metafield.d.ts.map +1 -0
  17. package/dist/asset-metafields.d.ts +27 -0
  18. package/dist/asset-metafields.d.ts.map +1 -0
  19. package/dist/asset-runtime.d.ts +218 -0
  20. package/dist/asset-runtime.d.ts.map +1 -0
  21. package/dist/asset-serving.d.ts +146 -0
  22. package/dist/asset-serving.d.ts.map +1 -0
  23. package/dist/asset-status.d.ts +15 -0
  24. package/dist/asset-status.d.ts.map +1 -0
  25. package/dist/asset-statuses.d.ts +25 -0
  26. package/dist/asset-statuses.d.ts.map +1 -0
  27. package/dist/asset-store.d.ts +200 -0
  28. package/dist/asset-store.d.ts.map +1 -0
  29. package/dist/asset-type.d.ts +15 -0
  30. package/dist/asset-type.d.ts.map +1 -0
  31. package/dist/asset-types.d.ts +28 -0
  32. package/dist/asset-types.d.ts.map +1 -0
  33. package/dist/asset.d.ts +158 -0
  34. package/dist/asset.d.ts.map +1 -0
  35. package/dist/assets.d.ts +125 -0
  36. package/dist/assets.d.ts.map +1 -0
  37. package/dist/folder.d.ts +16 -0
  38. package/dist/folder.d.ts.map +1 -0
  39. package/dist/folders.d.ts +45 -0
  40. package/dist/folders.d.ts.map +1 -0
  41. package/dist/index.d.ts +21 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +2285 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/manifest.json +4079 -0
  46. package/dist/media-bundle-persistence.d.ts +99 -0
  47. package/dist/media-bundle-persistence.d.ts.map +1 -0
  48. package/dist/owned-asset-helpers.d.ts +20 -0
  49. package/dist/owned-asset-helpers.d.ts.map +1 -0
  50. package/dist/playground.d.ts +2 -0
  51. package/dist/playground.d.ts.map +1 -0
  52. package/dist/playground.js +127 -0
  53. package/dist/playground.js.map +1 -0
  54. package/dist/smrt-knowledge.json +1922 -0
  55. package/dist/svelte/ActionBar.svelte +203 -0
  56. package/dist/svelte/ActionBar.svelte.d.ts +5 -0
  57. package/dist/svelte/ActionBar.svelte.d.ts.map +1 -0
  58. package/dist/svelte/AssetDetail.svelte +521 -0
  59. package/dist/svelte/AssetDetail.svelte.d.ts +35 -0
  60. package/dist/svelte/AssetDetail.svelte.d.ts.map +1 -0
  61. package/dist/svelte/AssetGrid.svelte +351 -0
  62. package/dist/svelte/AssetGrid.svelte.d.ts +5 -0
  63. package/dist/svelte/AssetGrid.svelte.d.ts.map +1 -0
  64. package/dist/svelte/AssetList.svelte +436 -0
  65. package/dist/svelte/AssetList.svelte.d.ts +5 -0
  66. package/dist/svelte/AssetList.svelte.d.ts.map +1 -0
  67. package/dist/svelte/AssetManager.svelte +381 -0
  68. package/dist/svelte/AssetManager.svelte.d.ts +5 -0
  69. package/dist/svelte/AssetManager.svelte.d.ts.map +1 -0
  70. package/dist/svelte/AssetToolbar.svelte +388 -0
  71. package/dist/svelte/AssetToolbar.svelte.d.ts +5 -0
  72. package/dist/svelte/AssetToolbar.svelte.d.ts.map +1 -0
  73. package/dist/svelte/CreateAssetModal.svelte +373 -0
  74. package/dist/svelte/CreateAssetModal.svelte.d.ts +19 -0
  75. package/dist/svelte/CreateAssetModal.svelte.d.ts.map +1 -0
  76. package/dist/svelte/__tests__/ActionBar.test.js +72 -0
  77. package/dist/svelte/__tests__/AssetDetail.test.js +57 -0
  78. package/dist/svelte/__tests__/AssetGrid.test.js +69 -0
  79. package/dist/svelte/__tests__/AssetList.test.js +72 -0
  80. package/dist/svelte/__tests__/AssetManager.test.js +21 -0
  81. package/dist/svelte/__tests__/AssetManagerRoute.test.js +16 -0
  82. package/dist/svelte/__tests__/AssetToolbar.test.js +39 -0
  83. package/dist/svelte/__tests__/CreateAssetModal.test.js +42 -0
  84. package/dist/svelte/i18n.d.ts +76 -0
  85. package/dist/svelte/i18n.d.ts.map +1 -0
  86. package/dist/svelte/i18n.js +87 -0
  87. package/dist/svelte/index.d.ts +19 -0
  88. package/dist/svelte/index.d.ts.map +1 -0
  89. package/dist/svelte/index.js +30 -0
  90. package/dist/svelte/playground/AssetDetailPreview.svelte +131 -0
  91. package/dist/svelte/playground/AssetDetailPreview.svelte.d.ts +8 -0
  92. package/dist/svelte/playground/AssetDetailPreview.svelte.d.ts.map +1 -0
  93. package/dist/svelte/playground/CreateAssetModalPreview.svelte +151 -0
  94. package/dist/svelte/playground/CreateAssetModalPreview.svelte.d.ts +4 -0
  95. package/dist/svelte/playground/CreateAssetModalPreview.svelte.d.ts.map +1 -0
  96. package/dist/svelte/playground.d.ts +60 -0
  97. package/dist/svelte/playground.d.ts.map +1 -0
  98. package/dist/svelte/playground.js +93 -0
  99. package/dist/svelte/routes/AssetManagerRoute.svelte +209 -0
  100. package/dist/svelte/routes/AssetManagerRoute.svelte.d.ts +4 -0
  101. package/dist/svelte/routes/AssetManagerRoute.svelte.d.ts.map +1 -0
  102. package/dist/svelte/routes/index.d.ts +2 -0
  103. package/dist/svelte/routes/index.d.ts.map +1 -0
  104. package/dist/svelte/routes/index.js +1 -0
  105. package/dist/svelte/routes/shared.d.ts +25 -0
  106. package/dist/svelte/routes/shared.d.ts.map +1 -0
  107. package/dist/svelte/routes/shared.js +31 -0
  108. package/dist/svelte/types.d.ts +179 -0
  109. package/dist/svelte/types.d.ts.map +1 -0
  110. package/dist/svelte/types.js +6 -0
  111. package/dist/types.d.ts +80 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +2 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/ui.d.ts +10 -0
  116. package/dist/ui.d.ts.map +1 -0
  117. package/dist/ui.js +85 -0
  118. package/dist/ui.js.map +1 -0
  119. package/package.json +102 -0
@@ -0,0 +1,373 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CreateAssetModal - Upload/create new assets
4
+ *
5
+ * Modal with drag-and-drop dropzone, paste support, and metadata fields.
6
+ * When a file is pasted or dragged onto the AssetManager, this modal
7
+ * opens with the file pre-loaded.
8
+ */
9
+
10
+ // Import primitives from subpaths (not the root barrel) so downstream
11
+ // consumers of `@happyvertical/smrt-assets/svelte` don't have to resolve the
12
+ // entire smrt-svelte surface — including optional peers like smrt-agents /
13
+ // smrt-users — just to compile this modal.
14
+ import { Modal } from '@happyvertical/smrt-ui/feedback';
15
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
16
+ import { Button } from '@happyvertical/smrt-ui/ui';
17
+ import { M } from './i18n.js';
18
+
19
+ const { t } = useI18n();
20
+
21
+ export interface CreateAssetModalProps {
22
+ /** Whether the modal is open */
23
+ open: boolean;
24
+ /** Pre-loaded file (from paste or drag) */
25
+ initialFile?: File | null;
26
+ /** Callback when creation is complete */
27
+ oncreate: (data: {
28
+ file: File;
29
+ name: string;
30
+ description: string;
31
+ altText: string;
32
+ }) => void;
33
+ /** Callback when modal is closed */
34
+ onclose: () => void;
35
+ }
36
+
37
+ let {
38
+ open,
39
+ initialFile = null,
40
+ oncreate,
41
+ onclose,
42
+ }: CreateAssetModalProps = $props();
43
+
44
+ let file = $state<File | null>(null);
45
+ let name = $state('');
46
+ let description = $state('');
47
+ let altText = $state('');
48
+ let dragOver = $state(false);
49
+ let previewUrl: string | null = $state(null);
50
+
51
+ // Sync initial file
52
+ $effect(() => {
53
+ if (initialFile) {
54
+ file = initialFile;
55
+ name = initialFile.name.replace(/\.[^/.]+$/, '');
56
+ }
57
+ });
58
+
59
+ // Preview URL for images
60
+ $effect(() => {
61
+ if (previewUrl) {
62
+ URL.revokeObjectURL(previewUrl);
63
+ }
64
+ if (file?.type.startsWith('image/')) {
65
+ previewUrl = URL.createObjectURL(file);
66
+ } else {
67
+ previewUrl = null;
68
+ }
69
+
70
+ return () => {
71
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
72
+ };
73
+ });
74
+
75
+ function handleDragOver(e: DragEvent) {
76
+ e.preventDefault();
77
+ dragOver = true;
78
+ }
79
+
80
+ function handleDragLeave() {
81
+ dragOver = false;
82
+ }
83
+
84
+ function handleDrop(e: DragEvent) {
85
+ e.preventDefault();
86
+ dragOver = false;
87
+ const dropped = e.dataTransfer?.files?.[0];
88
+ if (dropped) {
89
+ file = dropped;
90
+ name = dropped.name.replace(/\.[^/.]+$/, '');
91
+ }
92
+ }
93
+
94
+ function handleFileSelect(e: Event) {
95
+ const target = e.target as HTMLInputElement;
96
+ const selected = target.files?.[0];
97
+ if (selected) {
98
+ file = selected;
99
+ name = selected.name.replace(/\.[^/.]+$/, '');
100
+ }
101
+ }
102
+
103
+ function handleSubmit() {
104
+ if (!file) return;
105
+ oncreate({ file, name, description, altText });
106
+ resetForm();
107
+ }
108
+
109
+ function handleClose() {
110
+ resetForm();
111
+ onclose();
112
+ }
113
+
114
+ function resetForm() {
115
+ file = null;
116
+ name = '';
117
+ description = '';
118
+ altText = '';
119
+ }
120
+
121
+ function formatSize(bytes: number): string {
122
+ if (bytes < 1024) return `${bytes} B`;
123
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
124
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
125
+ }
126
+
127
+ const isImage = $derived(file?.type?.startsWith('image/') ?? false);
128
+ const isLargeFile = $derived((file?.size ?? 0) > 2 * 1024 * 1024);
129
+ </script>
130
+
131
+ <Modal open={open} title={t(M['assets.create_asset_modal.upload_asset'])} onClose={handleClose} size="md">
132
+ {#if !file}
133
+ <!-- Dropzone -->
134
+ <div
135
+ class="dropzone"
136
+ class:dropzone--active={dragOver}
137
+ role="button"
138
+ tabindex="0"
139
+ ondragover={handleDragOver}
140
+ ondragleave={handleDragLeave}
141
+ ondrop={handleDrop}
142
+ onclick={() => document.getElementById('file-input')?.click()}
143
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') document.getElementById('file-input')?.click(); }}
144
+ >
145
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="dropzone__icon">
146
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
147
+ <polyline points="17 8 12 3 7 8"></polyline>
148
+ <line x1="12" y1="3" x2="12" y2="15"></line>
149
+ </svg>
150
+ <p class="dropzone__text">{t(M['assets.create_asset_modal.dropzone_text'])}</p>
151
+ <p class="dropzone__hint">{t(M['assets.create_asset_modal.dropzone_hint'])}</p>
152
+ <input id="file-input" type="file" class="dropzone__input" onchange={handleFileSelect} />
153
+ </div>
154
+ {:else}
155
+ <!-- File preview + metadata form -->
156
+ <div class="file-preview">
157
+ {#if previewUrl}
158
+ <img src={previewUrl} alt={t(M['assets.create_asset_modal.preview_alt'])} class="file-preview__image" />
159
+ {:else}
160
+ <div class="file-preview__icon">📎</div>
161
+ {/if}
162
+ <div class="file-preview__meta">
163
+ <span class="file-preview__filename">{file.name}</span>
164
+ <span class="file-preview__size">{formatSize(file.size)}</span>
165
+ {#if isLargeFile}
166
+ <span class="file-preview__warning">{t(M['assets.create_asset_modal.large_file_warning'])}</span>
167
+ {/if}
168
+ </div>
169
+ <button type="button" class="file-preview__remove" onclick={() => { file = null; }} aria-label={t(M['assets.create_asset_modal.remove_file'])}>
170
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
171
+ </button>
172
+ </div>
173
+
174
+ <div class="form-fields">
175
+ <div class="form-field">
176
+ <label for="asset-name" class="form-label">Name</label>
177
+ <input id="asset-name" type="text" class="form-input" bind:value={name} placeholder={t(M['assets.create_asset_modal.name_placeholder'])} />
178
+ </div>
179
+
180
+ <div class="form-field">
181
+ <label for="asset-desc" class="form-label">Description</label>
182
+ <textarea id="asset-desc" class="form-textarea" bind:value={description} placeholder={t(M['assets.create_asset_modal.description_placeholder'])} rows="2"></textarea>
183
+ </div>
184
+
185
+ {#if isImage}
186
+ <div class="form-field">
187
+ <label for="asset-alt" class="form-label">
188
+ {t(M['assets.create_asset_modal.alt_text'])}
189
+ {#if !altText}
190
+ <span class="form-label__warning">{t(M['assets.create_asset_modal.alt_text_recommended_warning'])}</span>
191
+ {/if}
192
+ </label>
193
+ <input id="asset-alt" type="text" class="form-input" bind:value={altText} placeholder={t(M['assets.create_asset_modal.alt_text_placeholder'])} />
194
+ </div>
195
+ {/if}
196
+ </div>
197
+ {/if}
198
+
199
+ {#snippet footer()}
200
+ <Button variant="ghost" size="sm" onclick={handleClose}>Cancel</Button>
201
+ <Button variant="primary" size="sm" onclick={handleSubmit} disabled={!file}>
202
+ Upload
203
+ </Button>
204
+ {/snippet}
205
+ </Modal>
206
+
207
+ <style>
208
+ /* Dropzone */
209
+ .dropzone {
210
+ display: flex;
211
+ flex-direction: column;
212
+ align-items: center;
213
+ justify-content: center;
214
+ padding: var(--smrt-spacing-8, 2rem);
215
+ border: 2px dashed var(--smrt-color-outline-variant, #d1d5db);
216
+ border-radius: var(--smrt-radius-medium, 0.5rem);
217
+ cursor: pointer;
218
+ transition: all 150ms ease;
219
+ text-align: center;
220
+ }
221
+
222
+ .dropzone:hover, .dropzone--active {
223
+ border-color: var(--smrt-color-primary, #005ac1);
224
+ background: var(--smrt-color-primary-container, rgba(0, 90, 193, 0.05));
225
+ }
226
+
227
+ .dropzone__icon {
228
+ color: var(--smrt-color-on-surface-variant, #6b7280);
229
+ margin-bottom: var(--smrt-spacing-3, 0.75rem);
230
+ }
231
+
232
+ .dropzone__text {
233
+ margin: 0;
234
+ font-weight: var(--smrt-typography-weight-medium, 500);
235
+ color: var(--smrt-color-on-surface, #111827);
236
+ }
237
+
238
+ .dropzone__hint {
239
+ margin: var(--smrt-spacing-1, 0.25rem) 0 0;
240
+ font-size: var(--smrt-typography-body-small-size, 0.8rem);
241
+ color: var(--smrt-color-on-surface-variant, #9ca3af);
242
+ }
243
+
244
+ .dropzone__input {
245
+ display: none;
246
+ }
247
+
248
+ /* File preview */
249
+ .file-preview {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: var(--smrt-spacing-3, 0.75rem);
253
+ padding: var(--smrt-spacing-3, 0.75rem);
254
+ background: var(--smrt-color-surface-container-low, #f9fafb);
255
+ border-radius: var(--smrt-radius-medium, 0.5rem);
256
+ margin-bottom: var(--smrt-spacing-4, 1rem);
257
+ }
258
+
259
+ .file-preview__image {
260
+ width: 60px;
261
+ height: 60px;
262
+ object-fit: cover;
263
+ border-radius: var(--smrt-radius-small, 0.25rem);
264
+ }
265
+
266
+ .file-preview__icon {
267
+ width: 60px;
268
+ height: 60px;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ font-size: var(--smrt-typography-headline-small-size, 1.5rem);
273
+ background: var(--smrt-color-surface-container, #f3f4f6);
274
+ border-radius: var(--smrt-radius-small, 0.25rem);
275
+ }
276
+
277
+ .file-preview__meta {
278
+ flex: 1;
279
+ min-width: 0;
280
+ }
281
+
282
+ .file-preview__filename {
283
+ display: block;
284
+ font-weight: var(--smrt-typography-weight-medium, 500);
285
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
286
+ white-space: nowrap;
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ }
290
+
291
+ .file-preview__size {
292
+ display: block;
293
+ font-size: var(--smrt-typography-label-medium-size, 0.75rem);
294
+ color: var(--smrt-color-on-surface-variant, #6b7280);
295
+ }
296
+
297
+ .file-preview__warning {
298
+ display: block;
299
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
300
+ color: var(--smrt-color-error, #dc2626);
301
+ margin-top: var(--smrt-spacing-1, 4px);
302
+ }
303
+
304
+ .file-preview__remove {
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ width: 28px;
309
+ height: 28px;
310
+ flex-shrink: 0;
311
+ padding: 0;
312
+ border: none;
313
+ background: transparent;
314
+ color: var(--smrt-color-on-surface-variant, #6b7280);
315
+ cursor: pointer;
316
+ border-radius: var(--smrt-radius-full, 9999px);
317
+ }
318
+
319
+ .file-preview__remove:hover {
320
+ background: var(--smrt-color-surface-container, #f3f4f6);
321
+ color: var(--smrt-color-error, #dc2626);
322
+ }
323
+
324
+ /* Form fields */
325
+ .form-fields {
326
+ display: flex;
327
+ flex-direction: column;
328
+ gap: var(--smrt-spacing-3, 0.75rem);
329
+ }
330
+
331
+ .form-field {
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: var(--smrt-spacing-1, 0.25rem);
335
+ }
336
+
337
+ .form-label {
338
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
339
+ font-weight: var(--smrt-typography-weight-medium, 500);
340
+ color: var(--smrt-color-on-surface, #111827);
341
+ }
342
+
343
+ .form-label__warning {
344
+ font-weight: var(--smrt-typography-weight-normal, 400);
345
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
346
+ color: var(--smrt-color-error, #dc2626);
347
+ margin-left: var(--smrt-spacing-1, 4px);
348
+ }
349
+
350
+ .form-input, .form-textarea {
351
+ width: 100%;
352
+ padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-3, 0.75rem);
353
+ border: 1px solid var(--smrt-color-outline-variant, #e5e7eb);
354
+ border-radius: var(--smrt-radius-medium, 0.5rem);
355
+ font-family: inherit;
356
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
357
+ color: var(--smrt-color-on-surface, #111827);
358
+ background: var(--smrt-color-surface, #ffffff);
359
+ box-sizing: border-box;
360
+ }
361
+
362
+ .form-input:focus, .form-textarea:focus {
363
+ outline: none;
364
+ border-color: var(--smrt-color-primary, #005ac1);
365
+ box-shadow: 0 0 0 2px var(--smrt-color-primary-container, rgba(0, 90, 193, 0.1));
366
+ }
367
+
368
+ .form-textarea {
369
+ resize: vertical;
370
+ min-height: 60px;
371
+ }
372
+
373
+ </style>
@@ -0,0 +1,19 @@
1
+ export interface CreateAssetModalProps {
2
+ /** Whether the modal is open */
3
+ open: boolean;
4
+ /** Pre-loaded file (from paste or drag) */
5
+ initialFile?: File | null;
6
+ /** Callback when creation is complete */
7
+ oncreate: (data: {
8
+ file: File;
9
+ name: string;
10
+ description: string;
11
+ altText: string;
12
+ }) => void;
13
+ /** Callback when modal is closed */
14
+ onclose: () => void;
15
+ }
16
+ declare const CreateAssetModal: import("svelte").Component<CreateAssetModalProps, {}, "">;
17
+ type CreateAssetModal = ReturnType<typeof CreateAssetModal>;
18
+ export default CreateAssetModal;
19
+ //# sourceMappingURL=CreateAssetModal.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CreateAssetModal.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/CreateAssetModal.svelte.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,qBAAqB;IACpC,gCAAgC;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,2CAA2C;IAC3C,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,IAAI,EAAE,IAAI,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,IAAI,CAAC;IACX,oCAAoC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAoLD,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,72 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * First component test in a domain package via the shared S11 harness (#1416):
4
+ * the whole Testing-Library + axe surface comes from `@happyvertical/smrt-vitest`
5
+ * (no per-package testing-library deps), and the jsdom `<dialog>` polyfill +
6
+ * jest-dom matchers come from the shared `svelte-setup` in vitest.config.
7
+ */
8
+ import { expectNoA11yViolations, render, screen, userEvent, within, } from '@happyvertical/smrt-vitest/svelte';
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import ActionBar from '../ActionBar.svelte';
11
+ // ActionBar only reads `selectedAssets.length` and forwards the array to its
12
+ // callbacks, so minimal id-only stubs are enough to drive it.
13
+ const selected = [{ id: '1' }, { id: '2' }];
14
+ describe('ActionBar', () => {
15
+ it('shows the selected count and clears the selection', async () => {
16
+ const onClearSelection = vi.fn();
17
+ render(ActionBar, {
18
+ props: { selectedAssets: selected, onClearSelection, onDelete: vi.fn() },
19
+ });
20
+ expect(screen.getByText('2 selected')).toBeInTheDocument();
21
+ await userEvent.click(screen.getByRole('button', { name: 'Clear' }));
22
+ expect(onClearSelection).toHaveBeenCalledTimes(1);
23
+ });
24
+ it('renders nothing when no assets are selected', () => {
25
+ const { container } = render(ActionBar, {
26
+ props: {
27
+ selectedAssets: [],
28
+ onClearSelection: vi.fn(),
29
+ onDelete: vi.fn(),
30
+ },
31
+ });
32
+ expect(container.querySelector('.action-bar')).toBeNull();
33
+ });
34
+ it('confirms deletion through the inline dialog', async () => {
35
+ const onDelete = vi.fn();
36
+ render(ActionBar, {
37
+ props: { selectedAssets: selected, onClearSelection: vi.fn(), onDelete },
38
+ });
39
+ await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
40
+ // The library ConfirmDialog (S10) labels its dialog by the title heading.
41
+ // Asserting the interpolated title AND message proves Svelte attribute-string
42
+ // interpolation (`"… {count} …"`) resolves for component props (count = 2).
43
+ const dialog = screen.getByRole('dialog', { name: /Delete 2 assets\?/ });
44
+ expect(within(dialog).getByText(/This action cannot be undone\./)).toBeInTheDocument();
45
+ await userEvent.click(within(dialog).getByRole('button', { name: 'Delete' }));
46
+ expect(onDelete).toHaveBeenCalledWith(selected);
47
+ });
48
+ it('cancels deletion and dismisses the dialog', async () => {
49
+ render(ActionBar, {
50
+ props: {
51
+ selectedAssets: selected,
52
+ onClearSelection: vi.fn(),
53
+ onDelete: vi.fn(),
54
+ },
55
+ });
56
+ await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
57
+ await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', {
58
+ name: 'Cancel',
59
+ }));
60
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
61
+ });
62
+ it('is axe-clean', async () => {
63
+ const { container } = render(ActionBar, {
64
+ props: {
65
+ selectedAssets: selected,
66
+ onClearSelection: vi.fn(),
67
+ onDelete: vi.fn(),
68
+ },
69
+ });
70
+ await expectNoA11yViolations(container);
71
+ });
72
+ });
@@ -0,0 +1,57 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for AssetDetail via the shared S11 harness (#1416).
4
+ * `open: true` drives the <dialog> showModal effect (polyfilled in the shared
5
+ * setup), so these exercise the real open state.
6
+ */
7
+ import { render, screen, userEvent } from '@happyvertical/smrt-vitest/svelte';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+ import AssetDetail from '../AssetDetail.svelte';
10
+ const asset = {
11
+ id: '1',
12
+ name: 'photo.png',
13
+ mimeType: 'image/png',
14
+ typeSlug: 'image',
15
+ statusSlug: 'published',
16
+ sourceUri: 'file:///photo.png',
17
+ description: 'a picture',
18
+ metadata: '',
19
+ };
20
+ const baseProps = (over = {}) => ({
21
+ asset,
22
+ open: true,
23
+ onClose: vi.fn(),
24
+ ...over,
25
+ });
26
+ describe('AssetDetail', () => {
27
+ it('renders the detail dialog for an asset', () => {
28
+ render(AssetDetail, { props: baseProps() });
29
+ expect(screen.getByRole('heading', { name: 'photo.png', hidden: true })).toBeInTheDocument();
30
+ });
31
+ it('closes via the close button', async () => {
32
+ const onClose = vi.fn();
33
+ render(AssetDetail, { props: baseProps({ onClose }) });
34
+ // The library Modal (S10) provides the dialog's close affordance, labelled
35
+ // "Close modal".
36
+ await userEvent.click(screen.getByRole('button', { name: 'Close modal', hidden: true }));
37
+ expect(onClose).toHaveBeenCalled();
38
+ });
39
+ it('deletes the asset', async () => {
40
+ const onDelete = vi.fn();
41
+ render(AssetDetail, { props: baseProps({ onDelete }) });
42
+ await userEvent.click(screen.getByRole('button', { name: 'Delete', hidden: true }));
43
+ expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ id: '1' }));
44
+ });
45
+ it('saves metadata edits', async () => {
46
+ const onSave = vi.fn();
47
+ render(AssetDetail, { props: baseProps({ onSave }) });
48
+ await userEvent.click(screen.getByRole('button', { name: 'Save', hidden: true }));
49
+ expect(onSave).toHaveBeenCalled();
50
+ });
51
+ it('renders nothing meaningful when closed with no asset', () => {
52
+ const { container } = render(AssetDetail, {
53
+ props: baseProps({ asset: null, open: false }),
54
+ });
55
+ expect(container.textContent).not.toContain('photo.png');
56
+ });
57
+ });
@@ -0,0 +1,69 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for AssetGrid via the shared S11 harness (#1416), plus the
4
+ * S12 a11y remediation (#1417): the whole-card open action is now a stretched
5
+ * "Open <name>" button instead of a role="button" card wrapping the checkbox
6
+ * (which tripped axe's nested-interactive rule), so axe is asserted here.
7
+ */
8
+ import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import AssetGrid from '../AssetGrid.svelte';
11
+ const assets = [
12
+ {
13
+ id: '1',
14
+ name: 'photo.png',
15
+ mimeType: 'image/png',
16
+ typeSlug: 'image',
17
+ statusSlug: 'published',
18
+ sourceUri: 'file:///1',
19
+ },
20
+ {
21
+ id: '2',
22
+ name: 'clip.mp4',
23
+ mimeType: 'video/mp4',
24
+ typeSlug: 'video',
25
+ statusSlug: 'published',
26
+ sourceUri: 'file:///2',
27
+ },
28
+ ];
29
+ const baseProps = (over = {}) => ({
30
+ assets,
31
+ selectedIds: new Set(),
32
+ onSelectionChange: vi.fn(),
33
+ onAssetClick: vi.fn(),
34
+ ...over,
35
+ });
36
+ describe('AssetGrid', () => {
37
+ it('renders a tile per asset', () => {
38
+ render(AssetGrid, { props: baseProps() });
39
+ expect(screen.getByText('photo.png')).toBeInTheDocument();
40
+ expect(screen.getByText('clip.mp4')).toBeInTheDocument();
41
+ });
42
+ it('opens an asset via its stretched open button', async () => {
43
+ const onAssetClick = vi.fn();
44
+ render(AssetGrid, { props: baseProps({ onAssetClick }) });
45
+ await userEvent.click(screen.getByRole('button', { name: 'Open photo.png' }));
46
+ expect(onAssetClick).toHaveBeenCalledWith(expect.objectContaining({ id: '1' }));
47
+ });
48
+ it('labels the card action "Select" in pick mode', () => {
49
+ render(AssetGrid, { props: baseProps({ mode: 'pick' }) });
50
+ expect(screen.getByRole('button', { name: 'Select photo.png' })).toBeInTheDocument();
51
+ expect(screen.queryByRole('button', { name: 'Open photo.png' })).toBeNull();
52
+ });
53
+ it('toggles selection via the per-asset checkbox', async () => {
54
+ const onSelectionChange = vi.fn();
55
+ render(AssetGrid, { props: baseProps({ onSelectionChange }) });
56
+ await userEvent.click(screen.getByLabelText('Select photo.png'));
57
+ expect(onSelectionChange).toHaveBeenCalledTimes(1);
58
+ });
59
+ it('renders without assets', () => {
60
+ const { container } = render(AssetGrid, {
61
+ props: baseProps({ assets: [] }),
62
+ });
63
+ expect(container.querySelector('*')).toBeTruthy();
64
+ });
65
+ it('is axe-clean', async () => {
66
+ const { container } = render(AssetGrid, { props: baseProps() });
67
+ await expectNoA11yViolations(container);
68
+ });
69
+ });
@@ -0,0 +1,72 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for AssetList via the shared S11 harness (#1416), plus the
4
+ * S12 a11y remediation (#1417): the row's open action is now a name button in
5
+ * its cell instead of a role="button" <tr> nesting the checkbox (axe
6
+ * nested-interactive), so axe is asserted here.
7
+ */
8
+ import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import AssetList from '../AssetList.svelte';
11
+ const assets = [
12
+ {
13
+ id: '1',
14
+ name: 'photo.png',
15
+ mimeType: 'image/png',
16
+ typeSlug: 'image',
17
+ statusSlug: 'published',
18
+ sourceUri: 'file:///1',
19
+ },
20
+ {
21
+ id: '2',
22
+ name: 'doc.pdf',
23
+ mimeType: 'application/pdf',
24
+ typeSlug: 'document',
25
+ statusSlug: 'published',
26
+ sourceUri: 'file:///2',
27
+ },
28
+ ];
29
+ const baseProps = (over = {}) => ({
30
+ assets,
31
+ selectedIds: new Set(),
32
+ sort: { field: 'name', direction: 'asc' },
33
+ onSelectionChange: vi.fn(),
34
+ onAssetClick: vi.fn(),
35
+ onSortChange: vi.fn(),
36
+ ...over,
37
+ });
38
+ describe('AssetList', () => {
39
+ it('renders a row per asset', () => {
40
+ render(AssetList, { props: baseProps() });
41
+ expect(screen.getByText('photo.png')).toBeInTheDocument();
42
+ expect(screen.getByText('doc.pdf')).toBeInTheDocument();
43
+ });
44
+ it('select-all toggles the selection', async () => {
45
+ const onSelectionChange = vi.fn();
46
+ render(AssetList, { props: baseProps({ onSelectionChange }) });
47
+ await userEvent.click(screen.getByLabelText('Select all'));
48
+ expect(onSelectionChange).toHaveBeenCalledTimes(1);
49
+ });
50
+ it('clicking a sortable column header re-sorts', async () => {
51
+ const onSortChange = vi.fn();
52
+ render(AssetList, { props: baseProps({ onSortChange }) });
53
+ await userEvent.click(screen.getByRole('button', { name: /Name/ }));
54
+ expect(onSortChange).toHaveBeenCalledTimes(1);
55
+ });
56
+ it('opens an asset via its name button', async () => {
57
+ const onAssetClick = vi.fn();
58
+ render(AssetList, { props: baseProps({ onAssetClick }) });
59
+ await userEvent.click(screen.getByRole('button', { name: 'photo.png' }));
60
+ expect(onAssetClick).toHaveBeenCalledWith(expect.objectContaining({ id: '1' }));
61
+ });
62
+ it('renders without assets', () => {
63
+ const { container } = render(AssetList, {
64
+ props: baseProps({ assets: [] }),
65
+ });
66
+ expect(container.querySelector('*')).toBeTruthy();
67
+ });
68
+ it('is axe-clean', async () => {
69
+ const { container } = render(AssetList, { props: baseProps() });
70
+ await expectNoA11yViolations(container);
71
+ });
72
+ });
@@ -0,0 +1,21 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for AssetManager (the asset UI shell) via the shared S11
4
+ * harness (#1416).
5
+ */
6
+ import { render, screen } from '@happyvertical/smrt-vitest/svelte';
7
+ import { describe, expect, it, vi } from 'vitest';
8
+ import AssetManager from '../AssetManager.svelte';
9
+ describe('AssetManager', () => {
10
+ it('renders the manager shell with its toolbar', () => {
11
+ render(AssetManager, { props: {} });
12
+ // The toolbar (AssetToolbar) renders its labelled search control.
13
+ expect(screen.getByLabelText('Search assets')).toBeInTheDocument();
14
+ });
15
+ it('renders in pick mode', () => {
16
+ const { container } = render(AssetManager, {
17
+ props: { mode: 'pick', onSelect: vi.fn() },
18
+ });
19
+ expect(container.querySelector('*')).toBeTruthy();
20
+ });
21
+ });