@jjlmoya/utils-audiovisual 1.2.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 (120) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +198 -0
  3. package/src/category/i18n/es.ts +198 -0
  4. package/src/category/i18n/fr.ts +198 -0
  5. package/src/category/index.ts +17 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +4 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/chromaticLens/bibliography.astro +17 -0
  23. package/src/tool/chromaticLens/component.astro +178 -0
  24. package/src/tool/chromaticLens/i18n/en.ts +246 -0
  25. package/src/tool/chromaticLens/i18n/es.ts +244 -0
  26. package/src/tool/chromaticLens/i18n/fr.ts +244 -0
  27. package/src/tool/chromaticLens/index.ts +43 -0
  28. package/src/tool/chromaticLens/logic.ts +87 -0
  29. package/src/tool/chromaticLens/seo.astro +15 -0
  30. package/src/tool/chromaticLens/style.css +308 -0
  31. package/src/tool/chromaticLens/ui.ts +109 -0
  32. package/src/tool/collageMaker/bibliography.astro +17 -0
  33. package/src/tool/collageMaker/component.astro +302 -0
  34. package/src/tool/collageMaker/i18n/en.ts +233 -0
  35. package/src/tool/collageMaker/i18n/es.ts +231 -0
  36. package/src/tool/collageMaker/i18n/fr.ts +231 -0
  37. package/src/tool/collageMaker/index.ts +51 -0
  38. package/src/tool/collageMaker/logic.ts +134 -0
  39. package/src/tool/collageMaker/seo.astro +15 -0
  40. package/src/tool/collageMaker/style.css +386 -0
  41. package/src/tool/exifCleaner/bibliography.astro +18 -0
  42. package/src/tool/exifCleaner/component.astro +162 -0
  43. package/src/tool/exifCleaner/i18n/en.ts +277 -0
  44. package/src/tool/exifCleaner/i18n/es.ts +277 -0
  45. package/src/tool/exifCleaner/i18n/fr.ts +277 -0
  46. package/src/tool/exifCleaner/index.ts +57 -0
  47. package/src/tool/exifCleaner/logic.ts +135 -0
  48. package/src/tool/exifCleaner/seo.astro +18 -0
  49. package/src/tool/exifCleaner/style.css +289 -0
  50. package/src/tool/exifCleaner/ui.ts +117 -0
  51. package/src/tool/imageCompressor/bibliography.astro +17 -0
  52. package/src/tool/imageCompressor/component.astro +262 -0
  53. package/src/tool/imageCompressor/i18n/en.ts +232 -0
  54. package/src/tool/imageCompressor/i18n/es.ts +230 -0
  55. package/src/tool/imageCompressor/i18n/fr.ts +230 -0
  56. package/src/tool/imageCompressor/index.ts +50 -0
  57. package/src/tool/imageCompressor/logic.ts +79 -0
  58. package/src/tool/imageCompressor/seo.astro +15 -0
  59. package/src/tool/imageCompressor/style.css +503 -0
  60. package/src/tool/printQualityCalculator/bibliography.astro +18 -0
  61. package/src/tool/printQualityCalculator/component.astro +318 -0
  62. package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
  63. package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
  64. package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
  65. package/src/tool/printQualityCalculator/index.ts +56 -0
  66. package/src/tool/printQualityCalculator/logic.ts +53 -0
  67. package/src/tool/printQualityCalculator/seo.astro +18 -0
  68. package/src/tool/printQualityCalculator/style.css +491 -0
  69. package/src/tool/printQualityCalculator/ui.ts +122 -0
  70. package/src/tool/privacyBlur/bibliography.astro +17 -0
  71. package/src/tool/privacyBlur/component.astro +230 -0
  72. package/src/tool/privacyBlur/i18n/en.ts +238 -0
  73. package/src/tool/privacyBlur/i18n/es.ts +236 -0
  74. package/src/tool/privacyBlur/i18n/fr.ts +236 -0
  75. package/src/tool/privacyBlur/index.ts +49 -0
  76. package/src/tool/privacyBlur/logic.ts +249 -0
  77. package/src/tool/privacyBlur/seo.astro +15 -0
  78. package/src/tool/privacyBlur/style.css +332 -0
  79. package/src/tool/privacyBlur/ui.ts +124 -0
  80. package/src/tool/subtitleSync/bibliography.astro +17 -0
  81. package/src/tool/subtitleSync/component.astro +187 -0
  82. package/src/tool/subtitleSync/i18n/en.ts +241 -0
  83. package/src/tool/subtitleSync/i18n/es.ts +241 -0
  84. package/src/tool/subtitleSync/i18n/fr.ts +241 -0
  85. package/src/tool/subtitleSync/index.ts +49 -0
  86. package/src/tool/subtitleSync/logic.ts +91 -0
  87. package/src/tool/subtitleSync/seo.astro +15 -0
  88. package/src/tool/subtitleSync/style.css +325 -0
  89. package/src/tool/subtitleSync/ui.ts +152 -0
  90. package/src/tool/timelapseCalculator/bibliography.astro +15 -0
  91. package/src/tool/timelapseCalculator/component.astro +148 -0
  92. package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
  93. package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
  94. package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
  95. package/src/tool/timelapseCalculator/index.ts +52 -0
  96. package/src/tool/timelapseCalculator/logic.ts +46 -0
  97. package/src/tool/timelapseCalculator/seo.astro +18 -0
  98. package/src/tool/timelapseCalculator/style.css +285 -0
  99. package/src/tool/tvDistance/bibliography.astro +17 -0
  100. package/src/tool/tvDistance/component.astro +178 -0
  101. package/src/tool/tvDistance/i18n/en.ts +223 -0
  102. package/src/tool/tvDistance/i18n/es.ts +223 -0
  103. package/src/tool/tvDistance/i18n/fr.ts +223 -0
  104. package/src/tool/tvDistance/index.ts +49 -0
  105. package/src/tool/tvDistance/logic.ts +47 -0
  106. package/src/tool/tvDistance/seo.astro +15 -0
  107. package/src/tool/tvDistance/style.css +435 -0
  108. package/src/tool/tvDistance/ui.ts +66 -0
  109. package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
  110. package/src/tool/videoFrameExtractor/component.astro +285 -0
  111. package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
  112. package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
  113. package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
  114. package/src/tool/videoFrameExtractor/index.ts +53 -0
  115. package/src/tool/videoFrameExtractor/logic.ts +49 -0
  116. package/src/tool/videoFrameExtractor/seo.astro +15 -0
  117. package/src/tool/videoFrameExtractor/style.css +426 -0
  118. package/src/tool/videoFrameExtractor/ui.ts +179 -0
  119. package/src/tools.ts +25 -0
  120. package/src/types.ts +72 -0
@@ -0,0 +1,491 @@
1
+ /* ── Print Quality Calculator ──────────────────────────── */
2
+ :root {
3
+ --pq-bg: #fff;
4
+ --pq-border: #cbd5e1;
5
+ --pq-primary: #6366f1;
6
+ --pq-primary-light: #e0e7ff;
7
+ --pq-accent: #4f46e5;
8
+ }
9
+
10
+ .theme-dark {
11
+ --pq-bg: #0f172a;
12
+ --pq-border: #334155;
13
+ --pq-primary: #6366f1;
14
+ --pq-primary-light: rgba(99, 102, 241, 0.2);
15
+ --pq-accent: #818cf8;
16
+ }
17
+
18
+ .pq-wrapper {
19
+ width: 100%;
20
+ max-width: 56rem;
21
+ margin: 0 auto;
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: 2rem;
25
+ padding: 1rem;
26
+ }
27
+
28
+ .pq-hidden {
29
+ display: none;
30
+ }
31
+
32
+ /* Drop zone */
33
+ .pq-drop-zone {
34
+ position: relative;
35
+ cursor: pointer;
36
+ border: 2px dashed var(--pq-border);
37
+ border-radius: 1rem;
38
+ padding: 3rem;
39
+ transition: border-color 0.3s, background 0.3s;
40
+ }
41
+
42
+ .theme-dark .pq-drop-zone {
43
+ border-color: var(--pq-border);
44
+ }
45
+ .pq-drop-zone:hover,
46
+ .pq-drop-zone.pq-drop-active {
47
+ border-color: var(--pq-primary);
48
+ background: rgba(99, 102, 241, 0.04);
49
+ }
50
+ .pq-drop-inner {
51
+ display: flex;
52
+ flex-direction: column;
53
+ align-items: center;
54
+ justify-content: center;
55
+ text-align: center;
56
+ gap: 1rem;
57
+ }
58
+ .pq-upload-icon-wrap {
59
+ width: 5rem;
60
+ height: 5rem;
61
+ background: var(--pq-primary-light);
62
+ border-radius: 50%;
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ color: var(--pq-accent);
67
+ transition: transform 0.3s;
68
+ }
69
+
70
+ .theme-dark .pq-upload-icon-wrap {
71
+ background: rgba(99, 102, 241, 0.2);
72
+ color: #818cf8;
73
+ }
74
+ .pq-drop-zone:hover .pq-upload-icon-wrap {
75
+ transform: scale(1.1);
76
+ }
77
+ .pq-drop-title {
78
+ font-size: 1.25rem;
79
+ font-weight: 700;
80
+ color: #0f172a;
81
+ margin: 0;
82
+ }
83
+
84
+ .theme-dark .pq-drop-title {
85
+ color: #f1f5f9;
86
+ }
87
+ .pq-drop-sub {
88
+ color: #64748b;
89
+ margin: 0.25rem 0 0;
90
+ font-size: 0.95rem;
91
+ }
92
+ .pq-drop-overlay {
93
+ position: absolute;
94
+ inset: 0;
95
+ background: linear-gradient(135deg, rgba(99,102,241,0.04), rgba(168,85,247,0.04));
96
+ border-radius: 1rem;
97
+ opacity: 0;
98
+ pointer-events: none;
99
+ transition: opacity 0.3s;
100
+ }
101
+ .pq-drop-zone:hover .pq-drop-overlay { opacity: 1; }
102
+
103
+ /* Results */
104
+ .pq-results {
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 2rem;
108
+ animation: pq-fade-up 0.5s ease-out;
109
+ }
110
+
111
+ @keyframes pq-fade-up {
112
+ from {
113
+ opacity: 0;
114
+ transform: translateY(1.25rem);
115
+ }
116
+ to {
117
+ opacity: 1;
118
+ transform: translateY(0);
119
+ }
120
+ }
121
+
122
+ /* File card */
123
+ .pq-file-card {
124
+ background: #fff;
125
+ border: 1px solid #e2e8f0;
126
+ border-radius: 0.75rem;
127
+ box-shadow: 0 4px 24px -8px rgba(0,0,0,0.1);
128
+ padding: 1.5rem;
129
+ }
130
+
131
+ .theme-dark .pq-file-card {
132
+ background: #0f172a;
133
+ border-color: #1e293b;
134
+ }
135
+ .pq-file-left {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 1.5rem;
139
+ }
140
+ .pq-preview-wrap {
141
+ position: relative;
142
+ width: 8rem;
143
+ height: 8rem;
144
+ flex-shrink: 0;
145
+ }
146
+ .pq-preview-img {
147
+ width: 100%;
148
+ height: 100%;
149
+ object-fit: cover;
150
+ border-radius: 0.5rem;
151
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
152
+ }
153
+ .pq-file-name {
154
+ font-weight: 700;
155
+ color: #0f172a;
156
+ font-size: 1rem;
157
+ margin: 0 0 0.5rem;
158
+ overflow: hidden;
159
+ text-overflow: ellipsis;
160
+ white-space: nowrap;
161
+ max-width: 20rem;
162
+ }
163
+
164
+ .theme-dark .pq-file-name {
165
+ color: #f1f5f9;
166
+ }
167
+ .pq-file-meta {
168
+ display: flex;
169
+ flex-wrap: wrap;
170
+ gap: 1rem;
171
+ }
172
+ .pq-meta-item {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 0.25rem;
176
+ font-size: 0.85rem;
177
+ color: #64748b;
178
+ }
179
+
180
+ /* Main grid */
181
+ .pq-main-grid {
182
+ display: grid;
183
+ grid-template-columns: 1fr 1fr;
184
+ gap: 2rem;
185
+ }
186
+
187
+ @media (max-width: 640px) {
188
+ .pq-main-grid { grid-template-columns: 1fr; }
189
+ }
190
+
191
+ /* Config panel */
192
+ .pq-config-panel {
193
+ background: #f8fafc;
194
+ border: 1px solid #e2e8f0;
195
+ border-radius: 0.75rem;
196
+ padding: 1.5rem;
197
+ }
198
+
199
+ .theme-dark .pq-config-panel {
200
+ background: rgba(30, 41, 59, 0.5);
201
+ border-color: var(--pq-border);
202
+ }
203
+ .pq-config-label {
204
+ display: block;
205
+ font-size: 0.875rem;
206
+ font-weight: 500;
207
+ color: #475569;
208
+ margin-bottom: 1rem;
209
+ }
210
+
211
+ .theme-dark .pq-config-label {
212
+ color: #94a3b8;
213
+ }
214
+ .pq-slider {
215
+ width: 100%;
216
+ height: 0.5rem;
217
+ background: #e2e8f0;
218
+ border-radius: 0.5rem;
219
+ appearance: none;
220
+ cursor: pointer;
221
+ accent-color: #6366f1;
222
+ margin-bottom: 1rem;
223
+ }
224
+
225
+ .theme-dark .pq-slider {
226
+ background: #334155;
227
+ }
228
+ .pq-dpi-row {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 1rem;
232
+ margin-bottom: 1.5rem;
233
+ }
234
+ .pq-dpi-number {
235
+ width: 6rem;
236
+ border: 1px solid #e2e8f0;
237
+ border-radius: 0.5rem;
238
+ background: #fff;
239
+ color: #0f172a;
240
+ font-size: 1rem;
241
+ font-weight: 700;
242
+ text-align: center;
243
+ padding: 0.4rem 0.5rem;
244
+ outline: none;
245
+ }
246
+ .pq-dpi-number:focus { border-color: var(--pq-primary); }
247
+
248
+ .theme-dark .pq-dpi-number {
249
+ background: #0f172a;
250
+ border-color: var(--pq-border);
251
+ color: #f1f5f9;
252
+ }
253
+ .pq-dpi-label {
254
+ font-size: 0.875rem;
255
+ color: #64748b;
256
+ }
257
+ .pq-presets {
258
+ display: flex;
259
+ flex-wrap: wrap;
260
+ gap: 0.5rem;
261
+ }
262
+ .pq-preset-btn {
263
+ padding: 0.25rem 0.75rem;
264
+ font-size: 0.75rem;
265
+ font-weight: 500;
266
+ border-radius: 999px;
267
+ background: #e2e8f0;
268
+ color: #475569;
269
+ border: none;
270
+ cursor: pointer;
271
+ transition: background 0.15s, color 0.15s;
272
+ }
273
+ .pq-preset-btn:hover { background: #cbd5e1; }
274
+
275
+ .theme-dark .pq-preset-btn {
276
+ background: #334155;
277
+ color: #94a3b8;
278
+ }
279
+
280
+ .theme-dark .pq-preset-btn:hover {
281
+ background: #475569;
282
+ }
283
+ .pq-preset-active {
284
+ background: var(--pq-primary-light);
285
+ color: #4338ca;
286
+ box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.2);
287
+ }
288
+
289
+ .theme-dark .pq-preset-active {
290
+ background: rgba(99, 102, 241, 0.3);
291
+ color: #a5b4fc;
292
+ }
293
+
294
+ /* Output panel – gradient */
295
+ .pq-output-panel {
296
+ display: flex;
297
+ }
298
+ .pq-output-gradient {
299
+ flex: 1;
300
+ display: flex;
301
+ flex-direction: column;
302
+ justify-content: center;
303
+ background: linear-gradient(135deg, #6366f1, #a855f7);
304
+ border-radius: 0.75rem;
305
+ padding: 1.5rem;
306
+ color: #fff;
307
+ box-shadow: 0 10px 40px -10px rgba(99,102,241,0.4);
308
+ position: relative;
309
+ overflow: hidden;
310
+ }
311
+ .pq-output-blur-bg {
312
+ position: absolute;
313
+ top: 0; right: 0;
314
+ width: 8rem; height: 8rem;
315
+ background: rgba(255,255,255,0.1);
316
+ border-radius: 50%;
317
+ filter: blur(2rem);
318
+ transform: translate(2rem, -2rem);
319
+ pointer-events: none;
320
+ }
321
+ .pq-output-subtitle {
322
+ font-size: 0.75rem;
323
+ font-weight: 500;
324
+ text-transform: uppercase;
325
+ letter-spacing: 0.05em;
326
+ color: rgba(224,231,255,0.9);
327
+ margin: 0 0 0.5rem;
328
+ position: relative;
329
+ }
330
+ .pq-output-size {
331
+ display: flex;
332
+ align-items: baseline;
333
+ gap: 0.5rem;
334
+ font-size: 2.5rem;
335
+ font-weight: 900;
336
+ margin-bottom: 0.25rem;
337
+ position: relative;
338
+ }
339
+ .pq-size-sep {
340
+ font-size: 1.5rem;
341
+ opacity: 0.5;
342
+ }
343
+ .pq-size-unit {
344
+ font-size: 1.5rem;
345
+ }
346
+ .pq-output-inches {
347
+ color: rgba(199, 210, 254, 0.8);
348
+ font-size: 0.875rem;
349
+ margin-bottom: 1.5rem;
350
+ position: relative;
351
+ }
352
+ .pq-quality-section {
353
+ padding-top: 1rem;
354
+ border-top: 1px solid rgba(255,255,255,0.2);
355
+ position: relative;
356
+ }
357
+ .pq-quality-badge {
358
+ display: inline-flex;
359
+ align-items: center;
360
+ gap: 0.375rem;
361
+ padding: 0.25rem 0.75rem;
362
+ border-radius: 999px;
363
+ font-size: 0.875rem;
364
+ font-weight: 700;
365
+ background: rgba(255,255,255,0.2);
366
+ backdrop-filter: blur(4px);
367
+ margin-bottom: 0.5rem;
368
+ }
369
+ .pq-badge-green {
370
+ background: rgba(16, 185, 129, 0.25);
371
+ box-shadow: 0 0 0 1px rgba(52, 211, 153, 0.3);
372
+ }
373
+
374
+ .pq-badge-yellow {
375
+ background: rgba(234, 179, 8, 0.25);
376
+ box-shadow: 0 0 0 1px rgba(253, 224, 71, 0.3);
377
+ }
378
+
379
+ .pq-badge-red {
380
+ background: rgba(239, 68, 68, 0.25);
381
+ box-shadow: 0 0 0 1px rgba(252, 165, 165, 0.3);
382
+ }
383
+
384
+ .pq-badge-slate {
385
+ background: rgba(100, 116, 139, 0.25);
386
+ box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.3);
387
+ }
388
+ .pq-quality-desc {
389
+ font-size: 0.875rem;
390
+ color: rgba(199,210,254,0.9);
391
+ line-height: 1.4;
392
+ margin: 0;
393
+ }
394
+
395
+ /* Formats */
396
+ .pq-formats-card {
397
+ background: #fff;
398
+ border: 1px solid #e2e8f0;
399
+ border-radius: 0.75rem;
400
+ padding: 1.5rem;
401
+ }
402
+
403
+ .theme-dark .pq-formats-card {
404
+ background: #0f172a;
405
+ border-color: #1e293b;
406
+ }
407
+ .pq-formats-title {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 0.5rem;
411
+ font-weight: 700;
412
+ font-size: 1rem;
413
+ color: #0f172a;
414
+ margin: 0 0 1rem;
415
+ }
416
+
417
+ .theme-dark .pq-formats-title {
418
+ color: #f1f5f9;
419
+ }
420
+ .pq-formats-scroll {
421
+ overflow-x: auto;
422
+ }
423
+ .pq-formats-table {
424
+ width: 100%;
425
+ border-collapse: collapse;
426
+ font-size: 0.875rem;
427
+ }
428
+ .pq-formats-table th {
429
+ text-align: left;
430
+ padding: 0.75rem 1rem;
431
+ font-size: 0.75rem;
432
+ font-weight: 700;
433
+ text-transform: uppercase;
434
+ letter-spacing: 0.05em;
435
+ color: #475569;
436
+ background: #f8fafc;
437
+ }
438
+
439
+ .theme-dark .pq-formats-table th {
440
+ background: #1e293b;
441
+ color: #94a3b8;
442
+ }
443
+ .pq-formats-row {
444
+ border-bottom: 1px solid #f1f5f9;
445
+ transition: background 0.15s;
446
+ }
447
+
448
+ .pq-formats-row:hover {
449
+ background: #f8fafc;
450
+ }
451
+
452
+ .theme-dark .pq-formats-row {
453
+ border-color: #1e293b;
454
+ }
455
+
456
+ .theme-dark .pq-formats-row:hover {
457
+ background: #1e293b;
458
+ }
459
+ .pq-td {
460
+ padding: 0.75rem 1rem;
461
+ color: #475569;
462
+ }
463
+
464
+ .theme-dark .pq-td {
465
+ color: #94a3b8;
466
+ }
467
+ .pq-td-name {
468
+ font-weight: 600;
469
+ color: #0f172a;
470
+ }
471
+
472
+ .theme-dark .pq-td-name {
473
+ color: #f1f5f9;
474
+ }
475
+ .pq-status {
476
+ display: inline-flex;
477
+ align-items: center;
478
+ gap: 0.35rem;
479
+ font-weight: 600;
480
+ font-size: 0.8rem;
481
+ }
482
+ .pq-status-ok { color: #16a34a; }
483
+ .pq-status-no { color: #d97706; }
484
+
485
+ .theme-dark .pq-status-ok {
486
+ color: #4ade80;
487
+ }
488
+
489
+ .theme-dark .pq-status-no {
490
+ color: #fbbf24;
491
+ }
@@ -0,0 +1,122 @@
1
+ import { calculatePrintSize, getQualityLevel, checkFormatSupport, PAPER_FORMATS } from './logic';
2
+ import type { PrintQualityCalculatorUI } from './index';
3
+
4
+ export function initPrintQualityCalculator() {
5
+ const root = document.getElementById('print-quality-root');
6
+ if (!root) return;
7
+
8
+ const labels = JSON.parse(root.dataset.ui || '{}') as PrintQualityCalculatorUI;
9
+
10
+ const fileInput = root.querySelector('#file-input') as HTMLInputElement;
11
+ const dropZone = root.querySelector('#drop-zone') as HTMLElement;
12
+ const resultsArea = root.querySelector('#results-area') as HTMLElement;
13
+ const emptyState = root.querySelector('#empty-state') as HTMLElement;
14
+
15
+ const previewImg = root.querySelector('#preview-img') as HTMLImageElement;
16
+ const fileNameEl = root.querySelector('#file-name') as HTMLElement;
17
+ const pixelDimsEl = root.querySelector('#pixel-dims') as HTMLElement;
18
+ const fileSizeEl = root.querySelector('#file-size') as HTMLElement;
19
+
20
+ const dpiSlider = root.querySelector('#dpi-slider') as HTMLInputElement;
21
+ const dpiInput = root.querySelector('#dpi-input') as HTMLInputElement;
22
+ const presets = root.querySelectorAll('.preset-btn') as NodeListOf<HTMLButtonElement>;
23
+
24
+ const outputCm = root.querySelector('#output-cm') as HTMLElement;
25
+ const outputIn = root.querySelector('#output-in') as HTMLElement;
26
+ const qualityLevel = root.querySelector('#quality-level') as HTMLElement;
27
+ const qualityDesc = root.querySelector('#quality-description') as HTMLElement;
28
+ const formatsBody = root.querySelector('#formats-body') as HTMLElement;
29
+
30
+ let currentPixels = { w: 0, h: 0 };
31
+
32
+ const updateUI = () => {
33
+ const dpi = parseInt(dpiInput.value) || 300;
34
+ const size = calculatePrintSize(currentPixels.w, currentPixels.h, dpi);
35
+
36
+ outputCm.textContent = `${size.cmW.toFixed(1)} x ${size.cmH.toFixed(1)} ${labels.unitCm}`;
37
+ outputIn.textContent = `${size.inW.toFixed(1)} x ${size.inH.toFixed(1)} ${labels.unitInches}`;
38
+
39
+ const level = getQualityLevel(dpi);
40
+ const levelKey = level.charAt(0).toUpperCase() + level.slice(1);
41
+
42
+ qualityLevel.textContent = labels[`quality${levelKey}`] || '';
43
+ qualityDesc.textContent = labels[`quality${levelKey}Desc`] || '';
44
+
45
+ renderFormats(size.cmW, size.cmH);
46
+ };
47
+
48
+ const renderFormats = (cmW: number, cmH: number) => {
49
+ formatsBody.innerHTML = "";
50
+ PAPER_FORMATS.forEach((paper) => {
51
+ const { supported, percentage } = checkFormatSupport(cmW, cmH, paper);
52
+ const row = document.createElement('tr');
53
+ row.innerHTML = `
54
+ <td class="format-name">${paper.name}</td>
55
+ <td class="format-measure">${paper.w} x ${paper.h} cm</td>
56
+ <td>
57
+ <div class="support-status ${supported ? 'status-yes' : 'status-no'}">
58
+ <span>${supported ? 'YES' : `${percentage.toFixed(0)}%`}</span>
59
+ </div>
60
+ </td>
61
+ `;
62
+ formatsBody.appendChild(row);
63
+ });
64
+ };
65
+
66
+ const handleFile = (file: File) => {
67
+ const reader = new FileReader();
68
+ reader.onload = (e) => {
69
+ const img = new Image();
70
+ img.onload = () => {
71
+ currentPixels = { w: img.naturalWidth, h: img.naturalHeight };
72
+ previewImg.src = img.src;
73
+ fileNameEl.textContent = file.name;
74
+ pixelDimsEl.textContent = `${img.naturalWidth} x ${img.naturalHeight} PX`;
75
+ fileSizeEl.textContent = `${(file.size / (1024 * 1024)).toFixed(2)} MB`;
76
+
77
+ resultsArea.classList.remove('hidden');
78
+ emptyState.classList.add('hidden');
79
+ updateUI();
80
+ resultsArea.scrollIntoView({ behavior: 'smooth', block: 'start' });
81
+ };
82
+ img.src = e.target?.result as string;
83
+ };
84
+ reader.readAsDataURL(file);
85
+ };
86
+
87
+ dropZone.onclick = () => fileInput.click();
88
+ fileInput.onchange = () => {
89
+ if (fileInput.files?.[0]) handleFile(fileInput.files[0]);
90
+ };
91
+
92
+ dpiSlider.oninput = () => {
93
+ dpiInput.value = dpiSlider.value;
94
+ presets.forEach(p => p.classList.remove('active'));
95
+ updateUI();
96
+ };
97
+
98
+ dpiInput.oninput = () => {
99
+ dpiSlider.value = dpiInput.value;
100
+ presets.forEach(p => p.classList.remove('active'));
101
+ updateUI();
102
+ };
103
+
104
+ presets.forEach(btn => {
105
+ btn.onclick = () => {
106
+ const val = btn.dataset.val!;
107
+ dpiInput.value = val;
108
+ dpiSlider.value = val;
109
+ presets.forEach(p => p.classList.remove('active'));
110
+ btn.classList.add('active');
111
+ updateUI();
112
+ };
113
+ });
114
+
115
+ root.ondragover = (e) => { e.preventDefault(); dropZone.classList.add('drop-zone-active'); };
116
+ root.ondragleave = () => { dropZone.classList.remove('drop-zone-active'); };
117
+ root.ondrop = (e: DragEvent) => {
118
+ e.preventDefault();
119
+ dropZone.classList.remove('drop-zone-active');
120
+ if (e.dataTransfer?.files[0]) handleFile(e.dataTransfer.files[0]);
121
+ };
122
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { privacyBlur } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await privacyBlur.i18n[locale]?.();
12
+ if (!content || !content.bibliography || content.bibliography.length === 0) return null;
13
+
14
+ const { bibliography, bibliographyTitle = 'Bibliografía' } = content;
15
+ ---
16
+
17
+ <SharedBibliography title={bibliographyTitle} links={bibliography} />