@jjlmoya/utils-audiovisual 1.5.0 → 1.7.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 (43) hide show
  1. package/package.json +58 -58
  2. package/src/category/i18n/fr.ts +1 -1
  3. package/src/tests/schemas_fulfillment.test.ts +23 -0
  4. package/src/tests/title_quality.test.ts +55 -0
  5. package/src/tool/chromaticLens/component.astro +38 -38
  6. package/src/tool/chromaticLens/i18n/en.ts +1 -1
  7. package/src/tool/chromaticLens/i18n/es.ts +1 -1
  8. package/src/tool/chromaticLens/i18n/fr.ts +1 -1
  9. package/src/tool/collageMaker/component.astro +47 -47
  10. package/src/tool/collageMaker/i18n/en.ts +1 -1
  11. package/src/tool/collageMaker/i18n/es.ts +1 -1
  12. package/src/tool/collageMaker/i18n/fr.ts +1 -1
  13. package/src/tool/exifCleaner/component.astro +49 -48
  14. package/src/tool/exifCleaner/i18n/en.ts +2 -2
  15. package/src/tool/exifCleaner/i18n/es.ts +2 -2
  16. package/src/tool/exifCleaner/i18n/fr.ts +3 -3
  17. package/src/tool/imageCompressor/component.astro +144 -106
  18. package/src/tool/imageCompressor/i18n/en.ts +12 -2
  19. package/src/tool/imageCompressor/i18n/es.ts +13 -3
  20. package/src/tool/imageCompressor/i18n/fr.ts +12 -2
  21. package/src/tool/imageCompressor/index.ts +10 -0
  22. package/src/tool/printQualityCalculator/component.astro +129 -104
  23. package/src/tool/printQualityCalculator/i18n/en.ts +17 -3
  24. package/src/tool/printQualityCalculator/i18n/es.ts +19 -5
  25. package/src/tool/printQualityCalculator/i18n/fr.ts +18 -4
  26. package/src/tool/printQualityCalculator/index.ts +14 -0
  27. package/src/tool/privacyBlur/component.astro +35 -35
  28. package/src/tool/privacyBlur/i18n/en.ts +1 -1
  29. package/src/tool/privacyBlur/i18n/es.ts +1 -1
  30. package/src/tool/privacyBlur/i18n/fr.ts +1 -1
  31. package/src/tool/subtitleSync/component.astro +42 -42
  32. package/src/tool/subtitleSync/i18n/en.ts +1 -1
  33. package/src/tool/subtitleSync/i18n/es.ts +1 -1
  34. package/src/tool/subtitleSync/i18n/fr.ts +3 -3
  35. package/src/tool/timelapseCalculator/component.astro +41 -42
  36. package/src/tool/tvDistance/component.astro +55 -55
  37. package/src/tool/tvDistance/i18n/en.ts +1 -1
  38. package/src/tool/tvDistance/i18n/es.ts +1 -1
  39. package/src/tool/tvDistance/i18n/fr.ts +1 -1
  40. package/src/tool/videoFrameExtractor/component.astro +54 -54
  41. package/src/tool/videoFrameExtractor/i18n/en.ts +1 -1
  42. package/src/tool/videoFrameExtractor/i18n/es.ts +1 -1
  43. package/src/tool/videoFrameExtractor/i18n/fr.ts +1 -1
@@ -1,7 +1,14 @@
1
1
  ---
2
+ import type { ImageCompressorUI } from './index';
3
+
4
+ interface Props {
5
+ ui: ImageCompressorUI;
6
+ }
7
+
8
+ const { ui } = Astro.props;
2
9
  ---
3
10
 
4
- <div class="ic-dashboard" id="image-compressor-root">
11
+ <div class="ic-dashboard" id="image-compressor-root" data-ui={JSON.stringify(ui)}>
5
12
 
6
13
  <div class="ic-global-settings">
7
14
  <div class="ic-toggle-group">
@@ -9,10 +16,10 @@
9
16
  <input type="checkbox" id="global-webp-toggle" checked />
10
17
  <span class="ic-webp-slider"></span>
11
18
  </label>
12
- <span class="ic-toggle-label">Convertir a WebP</span>
19
+ <span class="ic-toggle-label">{ui.convertToWebpLabel}</span>
13
20
  </div>
14
21
  <div class="ic-settings-group">
15
- <label for="global-quality">Compresión: <span id="global-q-val">80</span>%</label>
22
+ <label for="global-quality">{ui.compressionLabel}: <span id="global-q-val">80</span>%</label>
16
23
  <input type="range" id="global-quality" min="10" max="100" value="80" class="ic-mini-slider" />
17
24
  </div>
18
25
  </div>
@@ -24,21 +31,21 @@
24
31
  <path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M16 8l-4-4-4 4M12 4v12" />
25
32
  </svg>
26
33
  </span>
27
- <span class="ic-upload-text">Suelta tus imágenes aquí</span>
28
- <span class="ic-upload-subtext">Soporta múltiples archivos a la vez</span>
29
- <span class="ic-upload-btn">Explorar archivos</span>
34
+ <span class="ic-upload-text">{ui.dropTitle}</span>
35
+ <span class="ic-upload-subtext">{ui.dropSubtitle}</span>
36
+ <span class="ic-upload-btn">{ui.browseFilesBtn}</span>
30
37
  </label>
31
38
  <input type="file" id="image-input" accept="image/jpeg,image/png,image/webp" multiple />
32
39
  </div>
33
40
 
34
41
  <div id="file-list-container" class="ic-file-list-container" style="display:none">
35
42
  <div class="ic-list-header">
36
- <h3>Archivos Procesados</h3>
43
+ <h3>{ui.processedFilesTitle}</h3>
37
44
  <span id="total-savings" class="ic-total-savings"></span>
38
45
  </div>
39
46
  <ul id="file-list" class="ic-file-list"></ul>
40
47
  <div class="ic-global-actions">
41
- <button id="download-all" class="ic-primary-btn">Descargar Todas</button>
48
+ <button id="download-all" class="ic-primary-btn">{ui.downloadAllBtn}</button>
42
49
  </div>
43
50
  </div>
44
51
 
@@ -70,10 +77,10 @@
70
77
  <span class="ic-savings-pct"></span>
71
78
  </div>
72
79
  <div class="ic-item-actions">
73
- <button class="ic-icon-btn ic-edit-btn" title="Ajustar esta imagen">
80
+ <button class="ic-icon-btn ic-edit-btn" title={ui.adjustThisImage}>
74
81
  <svg viewBox="0 0 24 24" fill="none" width="18" height="18" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
75
82
  </button>
76
- <a class="ic-icon-btn ic-download-btn" title="Descargar" download>
83
+ <a class="ic-icon-btn ic-download-btn" title={ui.downloadTitle} download>
77
84
  <svg viewBox="0 0 24 24" fill="none" width="18" height="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 4v12M8 12l4 4 4-4"/></svg>
78
85
  </a>
79
86
  </div>
@@ -82,15 +89,15 @@
82
89
  <div class="ic-inline-editor" style="display:none">
83
90
  <div class="ic-editor-controls">
84
91
  <div class="ic-editor-group">
85
- <label>Calidad: <span class="ic-local-q-val">80</span>%</label>
92
+ <label>{ui.qualityLabel}: <span class="ic-local-q-val">80</span>%</label>
86
93
  <input type="range" class="ic-local-quality" min="10" max="100" value="80" />
87
94
  </div>
88
95
  <div class="ic-editor-group">
89
- <label>Ancho máx. (px):</label>
96
+ <label>{ui.maxWidthLabel}:</label>
90
97
  <input type="number" class="ic-local-width" placeholder="Original" />
91
98
  </div>
92
99
  </div>
93
- <button class="ic-editor-close">Cerrar</button>
100
+ <button class="ic-editor-close">{ui.closeBtn}</button>
94
101
  </div>
95
102
  </li>
96
103
  </template>
@@ -98,6 +105,9 @@
98
105
  <script>
99
106
  import { compressImage, formatBytes, generateId, type CompressorSettings } from './logic';
100
107
 
108
+ const root = document.getElementById('image-compressor-root');
109
+ const ui = root ? JSON.parse(root.dataset.ui || '{}') : {};
110
+
101
111
  interface QueueItem {
102
112
  id: string;
103
113
  file: File;
@@ -124,12 +134,12 @@
124
134
  const globalWebp = document.getElementById('global-webp-toggle') as HTMLInputElement;
125
135
 
126
136
  globalQuality?.addEventListener('input', () => {
127
- globalQVal.textContent = globalQuality.value;
137
+ if (globalQVal) globalQVal.textContent = globalQuality.value;
128
138
  });
129
139
 
130
140
  async function handleFiles(files: FileList | File[]) {
131
141
  if (!files.length) return;
132
- container.style.display = '';
142
+ if (container) container.style.display = '';
133
143
 
134
144
  for (let i = 0; i < files.length; i++) {
135
145
  const file = files[i];
@@ -137,7 +147,7 @@
137
147
 
138
148
  const id = generateId();
139
149
  const settings: CompressorSettings = {
140
- quality: parseInt(globalQuality.value),
150
+ quality: parseInt(globalQuality?.value || '80'),
141
151
  width: null,
142
152
  convertToWebp: globalWebp?.checked ?? true,
143
153
  };
@@ -152,12 +162,15 @@
152
162
  }
153
163
 
154
164
  function appendItem(item: QueueItem) {
165
+ if (!template || !fileList) return;
155
166
  const clone = template.content.cloneNode(true) as DocumentFragment;
156
167
  const li = clone.querySelector('li') as HTMLLIElement;
157
168
  li.dataset.id = item.id;
158
169
 
159
- (li.querySelector('.ic-filename') as HTMLElement).textContent = item.file.name;
160
- (li.querySelector('.ic-orig-size') as HTMLElement).textContent = formatBytes(item.originalSize);
170
+ const filenameElem = li.querySelector('.ic-filename') as HTMLElement;
171
+ const origSizeElem = li.querySelector('.ic-orig-size') as HTMLElement;
172
+ if (filenameElem) filenameElem.textContent = item.file.name;
173
+ if (origSizeElem) origSizeElem.textContent = formatBytes(item.originalSize);
161
174
 
162
175
  const localQ = li.querySelector('.ic-local-quality') as HTMLInputElement;
163
176
  const localQVal = li.querySelector('.ic-local-q-val') as HTMLElement;
@@ -166,20 +179,31 @@
166
179
  const editBtn = li.querySelector('.ic-edit-btn') as HTMLButtonElement;
167
180
  const closeBtn = li.querySelector('.ic-editor-close') as HTMLButtonElement;
168
181
 
169
- localQ.value = item.settings.quality.toString();
170
- localQVal.textContent = item.settings.quality.toString();
182
+ if (localQ) {
183
+ localQ.value = item.settings.quality.toString();
184
+ if (localQVal) localQVal.textContent = item.settings.quality.toString();
185
+ }
171
186
 
172
- editBtn.addEventListener('click', () => {
173
- editor.style.display = editor.style.display === 'none' ? '' : 'none';
187
+ editBtn?.addEventListener('click', () => {
188
+ if (editor) {
189
+ editor.style.display = editor.style.display === 'none' ? '' : 'none';
190
+ }
191
+ });
192
+
193
+ closeBtn?.addEventListener('click', () => {
194
+ if (editor) editor.style.display = 'none';
174
195
  });
175
- closeBtn.addEventListener('click', () => { editor.style.display = 'none'; });
176
196
 
177
- localQ.addEventListener('input', () => { localQVal.textContent = localQ.value; });
178
- localQ.addEventListener('change', () => {
197
+ localQ?.addEventListener('input', () => {
198
+ if (localQVal) localQVal.textContent = localQ.value;
199
+ });
200
+
201
+ localQ?.addEventListener('change', () => {
179
202
  item.settings.quality = parseInt(localQ.value);
180
203
  processItem(item).then(() => updateTotals());
181
204
  });
182
- localW.addEventListener('change', () => {
205
+
206
+ localW?.addEventListener('change', () => {
183
207
  item.settings.width = localW.value ? parseInt(localW.value) : null;
184
208
  processItem(item).then(() => updateTotals());
185
209
  });
@@ -188,6 +212,7 @@
188
212
  }
189
213
 
190
214
  async function processItem(item: QueueItem): Promise<void> {
215
+ if (!fileList) return;
191
216
  const li = fileList.querySelector(`li[data-id="${item.id}"]`) as HTMLLIElement;
192
217
  const result = await compressImage(item.file, item.settings);
193
218
 
@@ -205,48 +230,61 @@
205
230
  const pill = li.querySelector('.ic-savings-pill') as HTMLElement;
206
231
  const downloadBtn = li.querySelector('.ic-download-btn') as HTMLAnchorElement;
207
232
 
208
- previewImg.src = result.dataUrl;
233
+ if (previewImg) previewImg.src = result.dataUrl;
209
234
 
210
- newSizeTxt.textContent = formatBytes(result.newSize);
235
+ if (newSizeTxt) newSizeTxt.textContent = formatBytes(result.newSize);
211
236
  const savings = ((item.originalSize - result.newSize) / item.originalSize) * 100;
212
237
 
213
- if (savings < 0) {
214
- savingsPct.textContent = `+${Math.abs(savings).toFixed(1)}%`;
215
- pill.classList.add('ic-savings-pill-negative');
216
- } else {
217
- savingsPct.textContent = `-${savings.toFixed(1)}%`;
218
- pill.classList.remove('ic-savings-pill-negative');
238
+ if (savingsPct && pill) {
239
+ if (savings < 0) {
240
+ savingsPct.textContent = `+${Math.abs(savings).toFixed(1)}%`;
241
+ pill.classList.add('ic-savings-pill-negative');
242
+ } else {
243
+ savingsPct.textContent = `-${savings.toFixed(1)}%`;
244
+ pill.classList.remove('ic-savings-pill-negative');
245
+ }
219
246
  }
220
247
 
221
248
  const baseName = item.file.name.replace(/\.[^/.]+$/, '');
222
- downloadBtn.href = result.dataUrl;
223
- downloadBtn.download = `${baseName}.${item.ext}`;
249
+ if (downloadBtn) {
250
+ downloadBtn.href = result.dataUrl;
251
+ downloadBtn.download = `${baseName}.${item.ext}`;
252
+ }
224
253
  }
225
254
 
226
255
  function updateTotals() {
256
+ if (!totalSavingsTxt) return;
227
257
  totalCompressedSize = Array.from(queue.values()).reduce((sum, i) => sum + i.newSize, 0);
228
258
  const saved = totalOriginalSize - totalCompressedSize;
229
259
  if (saved > 0) {
230
- totalSavingsTxt.textContent = `Ahorro total: ${formatBytes(saved)}`;
260
+ totalSavingsTxt.textContent = `${ui.totalSavingsLabel}: ${formatBytes(saved)}`;
231
261
  totalSavingsTxt.style.color = '#10b981';
232
262
  } else {
233
- totalSavingsTxt.textContent = 'Sin ahorro neto';
263
+ totalSavingsTxt.textContent = ui.noSavings || '';
234
264
  totalSavingsTxt.style.color = '#ef4444';
235
265
  }
236
266
  }
237
267
 
238
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('ic-dragover'); });
239
- dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('ic-dragover'); });
240
- dropZone.addEventListener('drop', (e) => {
268
+ dropZone?.addEventListener('dragover', (e) => {
269
+ e.preventDefault();
270
+ dropZone.classList.add('ic-dragover');
271
+ });
272
+
273
+ dropZone?.addEventListener('dragleave', () => {
274
+ dropZone.classList.remove('ic-dragover');
275
+ });
276
+
277
+ dropZone?.addEventListener('drop', (e) => {
241
278
  e.preventDefault();
242
279
  dropZone.classList.remove('ic-dragover');
243
280
  if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
244
281
  });
245
- fileInput.addEventListener('change', () => {
282
+
283
+ fileInput?.addEventListener('change', () => {
246
284
  if (fileInput.files?.length) handleFiles(fileInput.files);
247
285
  });
248
286
 
249
- downloadAllBtn.addEventListener('click', () => {
287
+ downloadAllBtn?.addEventListener('click', () => {
250
288
  queue.forEach((item) => {
251
289
  if (!item.dataUrl) return;
252
290
  const a = document.createElement('a');
@@ -261,7 +299,7 @@
261
299
  </script>
262
300
 
263
301
  <style>
264
- .ic-dashboard {
302
+ :global(.ic-dashboard) {
265
303
  max-width: 900px;
266
304
  margin: 0 auto;
267
305
  display: flex;
@@ -283,7 +321,7 @@
283
321
  --ic-shadow: rgba(0, 0, 0, 0.03);
284
322
  }
285
323
 
286
- :global(.theme-dark) .ic-dashboard {
324
+ :global(.theme-dark .ic-dashboard) {
287
325
  --ic-bg: #0f172a;
288
326
  --ic-bg-muted: #1e293b;
289
327
  --ic-border: #334155;
@@ -295,7 +333,7 @@
295
333
  --ic-shadow: rgba(0, 0, 0, 0.4);
296
334
  }
297
335
 
298
- .ic-global-settings {
336
+ :global(.ic-global-settings) {
299
337
  display: flex;
300
338
  justify-content: flex-end;
301
339
  gap: 1.5rem;
@@ -308,32 +346,32 @@
308
346
  flex-wrap: wrap;
309
347
  }
310
348
 
311
- :global(.theme-dark) .ic-global-settings {
349
+ :global(.theme-dark .ic-global-settings) {
312
350
  background: rgba(30, 41, 59, 0.6);
313
351
  border-color: rgba(71, 85, 105, 0.5);
314
352
  color: var(--ic-text-muted);
315
353
  }
316
354
 
317
- .ic-toggle-group {
355
+ :global(.ic-toggle-group) {
318
356
  display: flex;
319
357
  align-items: center;
320
358
  gap: 0.5rem;
321
359
  }
322
360
 
323
- .ic-webp-switch {
361
+ :global(.ic-webp-switch) {
324
362
  position: relative;
325
363
  display: inline-block;
326
364
  width: 44px;
327
365
  height: 24px;
328
366
  }
329
367
 
330
- .ic-webp-switch input {
368
+ :global(.ic-webp-switch input) {
331
369
  opacity: 0;
332
370
  width: 0;
333
371
  height: 0;
334
372
  }
335
373
 
336
- .ic-webp-slider {
374
+ :global(.ic-webp-slider) {
337
375
  position: absolute;
338
376
  cursor: pointer;
339
377
  inset: 0;
@@ -342,7 +380,7 @@
342
380
  transition: 0.3s;
343
381
  }
344
382
 
345
- .ic-webp-slider::before {
383
+ :global(.ic-webp-slider::before) {
346
384
  content: '';
347
385
  position: absolute;
348
386
  width: 18px;
@@ -355,20 +393,20 @@
355
393
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
356
394
  }
357
395
 
358
- .ic-webp-switch input:checked + .ic-webp-slider {
396
+ :global(.ic-webp-switch input:checked + .ic-webp-slider) {
359
397
  background: var(--ic-accent);
360
398
  }
361
399
 
362
- .ic-webp-switch input:checked + .ic-webp-slider::before {
400
+ :global(.ic-webp-switch input:checked + .ic-webp-slider::before) {
363
401
  transform: translateX(20px);
364
402
  }
365
403
 
366
- .ic-toggle-label {
404
+ :global(.ic-toggle-label) {
367
405
  font-size: 0.9rem;
368
406
  font-weight: 600;
369
407
  }
370
408
 
371
- .ic-settings-group {
409
+ :global(.ic-settings-group) {
372
410
  display: flex;
373
411
  align-items: center;
374
412
  gap: 0.75rem;
@@ -376,12 +414,12 @@
376
414
  font-weight: 600;
377
415
  }
378
416
 
379
- .ic-mini-slider {
417
+ :global(.ic-mini-slider) {
380
418
  width: 100px;
381
419
  accent-color: var(--ic-accent);
382
420
  }
383
421
 
384
- .ic-drop-zone {
422
+ :global(.ic-drop-zone) {
385
423
  position: relative;
386
424
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0.45) 100%);
387
425
  border: 3px dashed var(--ic-accent);
@@ -394,23 +432,23 @@
394
432
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.04);
395
433
  }
396
434
 
397
- :global(.theme-dark) .ic-drop-zone {
435
+ :global(.theme-dark .ic-drop-zone) {
398
436
  background: linear-gradient(180deg, rgba(30, 41, 59, 0.85) 0%, rgba(15, 23, 42, 0.6) 100%);
399
437
  }
400
438
 
401
- .ic-drop-zone.ic-dragover,
402
- .ic-drop-zone:hover {
439
+ :global(.ic-drop-zone.ic-dragover),
440
+ :global(.ic-drop-zone:hover) {
403
441
  transform: scale(1.01);
404
442
  border-color: var(--ic-accent-dark);
405
443
  background: linear-gradient(180deg, var(--ic-accent-light) 0%, rgba(255, 255, 255, 0.6) 100%);
406
444
  }
407
445
 
408
- :global(.theme-dark) .ic-drop-zone.ic-dragover,
409
- :global(.theme-dark) .ic-drop-zone:hover {
446
+ :global(.theme-dark .ic-drop-zone.ic-dragover),
447
+ :global(.theme-dark .ic-drop-zone:hover) {
410
448
  background: linear-gradient(180deg, rgba(6, 78, 59, 0.4) 0%, rgba(15, 23, 42, 0.6) 100%);
411
449
  }
412
450
 
413
- .ic-file-label {
451
+ :global(.ic-file-label) {
414
452
  display: flex;
415
453
  flex-direction: column;
416
454
  align-items: center;
@@ -419,24 +457,24 @@
419
457
  pointer-events: none;
420
458
  }
421
459
 
422
- .ic-upload-icon {
460
+ :global(.ic-upload-icon) {
423
461
  color: var(--ic-accent);
424
462
  margin-bottom: 0.5rem;
425
463
  }
426
464
 
427
- .ic-upload-text {
465
+ :global(.ic-upload-text) {
428
466
  font-size: 1.5rem;
429
467
  font-weight: 700;
430
468
  color: var(--ic-text);
431
469
  }
432
470
 
433
- .ic-upload-subtext {
471
+ :global(.ic-upload-subtext) {
434
472
  color: var(--ic-text-muted);
435
473
  font-size: 1rem;
436
474
  margin-bottom: 1rem;
437
475
  }
438
476
 
439
- .ic-upload-btn {
477
+ :global(.ic-upload-btn) {
440
478
  background: var(--ic-text);
441
479
  color: var(--ic-bg);
442
480
  padding: 0.75rem 2rem;
@@ -447,11 +485,11 @@
447
485
  transition: background 0.2s;
448
486
  }
449
487
 
450
- .ic-upload-btn:hover {
488
+ :global(.ic-upload-btn:hover) {
451
489
  background: var(--ic-text-muted);
452
490
  }
453
491
 
454
- .ic-drop-zone input[type="file"] {
492
+ :global(.ic-drop-zone input[type="file"]) {
455
493
  position: absolute;
456
494
  inset: 0;
457
495
  width: 100%;
@@ -460,11 +498,11 @@
460
498
  cursor: pointer;
461
499
  }
462
500
 
463
- .ic-file-list-container {
501
+ :global(.ic-file-list-container) {
464
502
  margin-top: 1.5rem;
465
503
  }
466
504
 
467
- .ic-list-header {
505
+ :global(.ic-list-header) {
468
506
  display: flex;
469
507
  justify-content: space-between;
470
508
  align-items: center;
@@ -472,19 +510,19 @@
472
510
  padding: 0 0.5rem;
473
511
  }
474
512
 
475
- .ic-list-header h3 {
513
+ :global(.ic-list-header h3) {
476
514
  font-size: 1.25rem;
477
515
  font-weight: 700;
478
516
  color: var(--ic-text);
479
517
  margin: 0;
480
518
  }
481
519
 
482
- .ic-total-savings {
520
+ :global(.ic-total-savings) {
483
521
  font-weight: 700;
484
522
  font-size: 1rem;
485
523
  }
486
524
 
487
- .ic-file-list {
525
+ :global(.ic-file-list) {
488
526
  list-style: none;
489
527
  padding: 1rem;
490
528
  margin: 0;
@@ -496,7 +534,7 @@
496
534
  border: 1px solid var(--ic-border);
497
535
  }
498
536
 
499
- .ic-file-item {
537
+ :global(.ic-file-item) {
500
538
  background: var(--ic-bg);
501
539
  border-radius: 0.75rem;
502
540
  padding: 0.75rem 1.25rem;
@@ -508,18 +546,18 @@
508
546
  transition: box-shadow 0.2s;
509
547
  }
510
548
 
511
- .ic-file-item:hover {
549
+ :global(.ic-file-item:hover) {
512
550
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
513
551
  }
514
552
 
515
- .ic-preview-col {
553
+ :global(.ic-preview-col) {
516
554
  display: flex;
517
555
  align-items: center;
518
556
  gap: 1rem;
519
557
  overflow: hidden;
520
558
  }
521
559
 
522
- .ic-preview-wrapper {
560
+ :global(.ic-preview-wrapper) {
523
561
  width: 48px;
524
562
  height: 48px;
525
563
  border-radius: 0.5rem;
@@ -528,19 +566,19 @@
528
566
  flex-shrink: 0;
529
567
  }
530
568
 
531
- .ic-preview-img {
569
+ :global(.ic-preview-img) {
532
570
  width: 100%;
533
571
  height: 100%;
534
572
  object-fit: cover;
535
573
  }
536
574
 
537
- .ic-file-info {
575
+ :global(.ic-file-info) {
538
576
  display: flex;
539
577
  flex-direction: column;
540
578
  overflow: hidden;
541
579
  }
542
580
 
543
- .ic-filename {
581
+ :global(.ic-filename) {
544
582
  font-weight: 600;
545
583
  color: var(--ic-text);
546
584
  white-space: nowrap;
@@ -549,17 +587,17 @@
549
587
  font-size: 0.9rem;
550
588
  }
551
589
 
552
- .ic-orig-size {
590
+ :global(.ic-orig-size) {
553
591
  color: var(--ic-text-muted);
554
592
  font-size: 0.8rem;
555
593
  }
556
594
 
557
- .ic-arrow-col {
595
+ :global(.ic-arrow-col) {
558
596
  display: flex;
559
597
  justify-content: center;
560
598
  }
561
599
 
562
- .ic-arrow-circle {
600
+ :global(.ic-arrow-circle) {
563
601
  background: var(--ic-accent-light);
564
602
  color: var(--ic-accent);
565
603
  width: 36px;
@@ -571,41 +609,41 @@
571
609
  flex-shrink: 0;
572
610
  }
573
611
 
574
- .ic-result-col {
612
+ :global(.ic-result-col) {
575
613
  display: flex;
576
614
  align-items: center;
577
615
  justify-content: flex-end;
578
616
  gap: 1.25rem;
579
617
  }
580
618
 
581
- .ic-savings-pill {
619
+ :global(.ic-savings-pill) {
582
620
  display: flex;
583
621
  flex-direction: column;
584
622
  align-items: center;
585
623
  }
586
624
 
587
- .ic-new-size {
625
+ :global(.ic-new-size) {
588
626
  font-weight: 700;
589
627
  color: var(--ic-text);
590
628
  font-size: 0.95rem;
591
629
  }
592
630
 
593
- .ic-savings-pct {
631
+ :global(.ic-savings-pct) {
594
632
  color: var(--ic-accent);
595
633
  font-weight: 800;
596
634
  font-size: 0.85rem;
597
635
  }
598
636
 
599
- .ic-savings-pill-negative .ic-savings-pct {
637
+ :global(.ic-savings-pill-negative .ic-savings-pct) {
600
638
  color: var(--ic-error);
601
639
  }
602
640
 
603
- .ic-item-actions {
641
+ :global(.ic-item-actions) {
604
642
  display: flex;
605
643
  gap: 0.4rem;
606
644
  }
607
645
 
608
- .ic-icon-btn {
646
+ :global(.ic-icon-btn) {
609
647
  background: var(--ic-bg-muted);
610
648
  border: none;
611
649
  color: var(--ic-text-muted);
@@ -621,22 +659,22 @@
621
659
  flex-shrink: 0;
622
660
  }
623
661
 
624
- .ic-icon-btn:hover {
662
+ :global(.ic-icon-btn:hover) {
625
663
  background: var(--ic-border);
626
664
  color: var(--ic-text);
627
665
  }
628
666
 
629
- .ic-download-btn {
667
+ :global(.ic-download-btn) {
630
668
  background: var(--ic-accent);
631
669
  color: var(--ic-bg);
632
670
  }
633
671
 
634
- .ic-download-btn:hover {
672
+ :global(.ic-download-btn:hover) {
635
673
  background: var(--ic-accent-dark);
636
674
  color: var(--ic-bg);
637
675
  }
638
676
 
639
- .ic-inline-editor {
677
+ :global(.ic-inline-editor) {
640
678
  grid-column: 1 / -1;
641
679
  background: var(--ic-bg-muted);
642
680
  border-radius: 0.75rem;
@@ -661,13 +699,13 @@
661
699
  }
662
700
  }
663
701
 
664
- .ic-editor-controls {
702
+ :global(.ic-editor-controls) {
665
703
  display: flex;
666
704
  gap: 1.5rem;
667
705
  flex-wrap: wrap;
668
706
  }
669
707
 
670
- .ic-editor-group {
708
+ :global(.ic-editor-group) {
671
709
  display: flex;
672
710
  flex-direction: column;
673
711
  gap: 0.25rem;
@@ -676,11 +714,11 @@
676
714
  color: var(--ic-text-muted);
677
715
  }
678
716
 
679
- .ic-editor-group input[type="range"] {
717
+ :global(.ic-editor-group input[type="range"]) {
680
718
  accent-color: var(--ic-accent);
681
719
  }
682
720
 
683
- .ic-editor-group input[type="number"] {
721
+ :global(.ic-editor-group input[type="number"]) {
684
722
  padding: 0.25rem 0.5rem;
685
723
  border: 1px solid var(--ic-border);
686
724
  border-radius: 0.375rem;
@@ -689,7 +727,7 @@
689
727
  color: var(--ic-text);
690
728
  }
691
729
 
692
- .ic-editor-close {
730
+ :global(.ic-editor-close) {
693
731
  background: var(--ic-border);
694
732
  border: none;
695
733
  padding: 0.4rem 0.875rem;
@@ -702,17 +740,17 @@
702
740
  white-space: nowrap;
703
741
  }
704
742
 
705
- .ic-editor-close:hover {
743
+ :global(.ic-editor-close:hover) {
706
744
  background: var(--ic-border);
707
745
  }
708
746
 
709
- .ic-global-actions {
747
+ :global(.ic-global-actions) {
710
748
  margin-top: 1.5rem;
711
749
  display: flex;
712
750
  justify-content: center;
713
751
  }
714
752
 
715
- .ic-primary-btn {
753
+ :global(.ic-primary-btn) {
716
754
  background: var(--ic-text);
717
755
  color: var(--ic-bg);
718
756
  border: none;
@@ -724,11 +762,11 @@
724
762
  transition: background 0.2s, transform 0.1s;
725
763
  }
726
764
 
727
- .ic-primary-btn:hover {
765
+ :global(.ic-primary-btn:hover) {
728
766
  background: var(--ic-text-muted);
729
767
  }
730
768
 
731
- .ic-primary-btn:active {
769
+ :global(.ic-primary-btn:active) {
732
770
  transform: scale(0.98);
733
771
  }
734
772