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