@jjlmoya/utils-social 1.1.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 (85) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +23 -0
  3. package/src/category/i18n/es.ts +23 -0
  4. package/src/category/i18n/fr.ts +23 -0
  5. package/src/category/index.ts +13 -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 +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +27 -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/redditFormatter/bibliography.astro +14 -0
  23. package/src/tool/redditFormatter/component.astro +246 -0
  24. package/src/tool/redditFormatter/i18n/en.ts +193 -0
  25. package/src/tool/redditFormatter/i18n/es.ts +193 -0
  26. package/src/tool/redditFormatter/i18n/fr.ts +193 -0
  27. package/src/tool/redditFormatter/index.ts +34 -0
  28. package/src/tool/redditFormatter/logic.ts +24 -0
  29. package/src/tool/redditFormatter/seo.astro +14 -0
  30. package/src/tool/redditFormatter/ui.ts +13 -0
  31. package/src/tool/safeZones/bibliography.astro +14 -0
  32. package/src/tool/safeZones/component.astro +857 -0
  33. package/src/tool/safeZones/i18n/en.ts +244 -0
  34. package/src/tool/safeZones/i18n/es.ts +244 -0
  35. package/src/tool/safeZones/i18n/fr.ts +244 -0
  36. package/src/tool/safeZones/index.ts +34 -0
  37. package/src/tool/safeZones/logic.ts +137 -0
  38. package/src/tool/safeZones/seo.astro +14 -0
  39. package/src/tool/safeZones/ui.ts +34 -0
  40. package/src/tool/socialImageResizer/bibliography.astro +14 -0
  41. package/src/tool/socialImageResizer/component.astro +498 -0
  42. package/src/tool/socialImageResizer/i18n/en.ts +211 -0
  43. package/src/tool/socialImageResizer/i18n/es.ts +211 -0
  44. package/src/tool/socialImageResizer/i18n/fr.ts +211 -0
  45. package/src/tool/socialImageResizer/index.ts +34 -0
  46. package/src/tool/socialImageResizer/logic.ts +133 -0
  47. package/src/tool/socialImageResizer/seo.astro +14 -0
  48. package/src/tool/socialImageResizer/ui.ts +7 -0
  49. package/src/tool/socialValueCalculator/bibliography.astro +14 -0
  50. package/src/tool/socialValueCalculator/component.astro +829 -0
  51. package/src/tool/socialValueCalculator/i18n/en.ts +310 -0
  52. package/src/tool/socialValueCalculator/i18n/es.ts +310 -0
  53. package/src/tool/socialValueCalculator/i18n/fr.ts +310 -0
  54. package/src/tool/socialValueCalculator/index.ts +34 -0
  55. package/src/tool/socialValueCalculator/logic.ts +60 -0
  56. package/src/tool/socialValueCalculator/seo.astro +14 -0
  57. package/src/tool/socialValueCalculator/ui.ts +31 -0
  58. package/src/tool/tinderPhotoOptimizer/bibliography.astro +14 -0
  59. package/src/tool/tinderPhotoOptimizer/component.astro +1514 -0
  60. package/src/tool/tinderPhotoOptimizer/i18n/en.ts +248 -0
  61. package/src/tool/tinderPhotoOptimizer/i18n/es.ts +248 -0
  62. package/src/tool/tinderPhotoOptimizer/i18n/fr.ts +248 -0
  63. package/src/tool/tinderPhotoOptimizer/index.ts +34 -0
  64. package/src/tool/tinderPhotoOptimizer/logic.ts +199 -0
  65. package/src/tool/tinderPhotoOptimizer/seo.astro +14 -0
  66. package/src/tool/tinderPhotoOptimizer/ui.ts +37 -0
  67. package/src/tool/youtubeThumbnail/bibliography.astro +14 -0
  68. package/src/tool/youtubeThumbnail/component.astro +419 -0
  69. package/src/tool/youtubeThumbnail/i18n/en.ts +207 -0
  70. package/src/tool/youtubeThumbnail/i18n/es.ts +207 -0
  71. package/src/tool/youtubeThumbnail/i18n/fr.ts +207 -0
  72. package/src/tool/youtubeThumbnail/index.ts +34 -0
  73. package/src/tool/youtubeThumbnail/logic.ts +7 -0
  74. package/src/tool/youtubeThumbnail/seo.astro +14 -0
  75. package/src/tool/youtubeThumbnail/ui.ts +13 -0
  76. package/src/tool/youtubeThumbnailPreviewer/bibliography.astro +14 -0
  77. package/src/tool/youtubeThumbnailPreviewer/component.astro +791 -0
  78. package/src/tool/youtubeThumbnailPreviewer/i18n/en.ts +321 -0
  79. package/src/tool/youtubeThumbnailPreviewer/i18n/es.ts +321 -0
  80. package/src/tool/youtubeThumbnailPreviewer/i18n/fr.ts +321 -0
  81. package/src/tool/youtubeThumbnailPreviewer/index.ts +34 -0
  82. package/src/tool/youtubeThumbnailPreviewer/seo.astro +14 -0
  83. package/src/tool/youtubeThumbnailPreviewer/ui.ts +19 -0
  84. package/src/tools.ts +10 -0
  85. package/src/types.ts +72 -0
@@ -0,0 +1,791 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { YoutubeThumbnailPreviewerUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const t = (ui ?? {}) as YoutubeThumbnailPreviewerUI;
12
+ ---
13
+
14
+ <div class="ytt-root">
15
+ <div class="premium-card">
16
+ <div class="main-layout">
17
+
18
+ <aside class="sidebar">
19
+ <div class="upload-group">
20
+ <div id="ytt-dropThumb" class="drop-zone" role="button" aria-label={t.dropFeedTitle}>
21
+ <div class="drop-icon">
22
+ <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
23
+ <rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/>
24
+ <rect x="2" y="14" width="8" height="8" rx="1"/><path d="M14 18h8M18 14v8"/>
25
+ </svg>
26
+ </div>
27
+ <div class="drop-text">
28
+ <h3>{t.dropFeedTitle}</h3>
29
+ <p>{t.dropFeedSub}</p>
30
+ </div>
31
+ <input type="file" id="ytt-thumbInput" accept="image/*" multiple style="display:none;" />
32
+ </div>
33
+
34
+ <div id="ytt-dropAvatar" class="drop-zone" role="button" aria-label={t.dropAvatarTitle}>
35
+ <div class="drop-icon">
36
+ <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
37
+ <circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
38
+ </svg>
39
+ </div>
40
+ <div class="drop-text">
41
+ <h3>{t.dropAvatarTitle}</h3>
42
+ <p>{t.dropAvatarSub}</p>
43
+ </div>
44
+ <input type="file" id="ytt-avatarInput" accept="image/*" style="display:none;" />
45
+ </div>
46
+ </div>
47
+
48
+ <div id="ytt-galleryPreview" class="gallery-preview"></div>
49
+
50
+ <div class="custom-settings">
51
+ <div class="settings-group">
52
+ <label for="ytt-mockupTitle">{t.labelTitle}</label>
53
+ <input type="text" id="ytt-mockupTitle" value={t.defaultTitle} placeholder={t.defaultTitle} />
54
+ </div>
55
+ <div class="settings-group">
56
+ <label for="ytt-mockupChannel">{t.labelChannel}</label>
57
+ <input type="text" id="ytt-mockupChannel" value={t.defaultChannel} placeholder={t.defaultChannel} />
58
+ </div>
59
+ </div>
60
+
61
+ <div class="tester-controls">
62
+ <button id="ytt-btnDesktop" class="control-btn active" data-view="desktop">
63
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
64
+ <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
65
+ </svg>
66
+ <span>{t.btnDesktop}</span>
67
+ </button>
68
+ <button id="ytt-btnMobile" class="control-btn" data-view="mobile">
69
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
70
+ <rect x="5" y="2" width="14" height="20" rx="2"/><circle cx="12" cy="17" r="1"/>
71
+ </svg>
72
+ <span>{t.btnMobile}</span>
73
+ </button>
74
+ <button id="ytt-btnReset" class="control-btn control-btn--reset">
75
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
76
+ <path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/>
77
+ </svg>
78
+ <span>{t.btnClear}</span>
79
+ </button>
80
+ </div>
81
+ </aside>
82
+
83
+ <main class="preview-area">
84
+ <div class="comparison-container mockup-desktop" id="ytt-comparisonContainer">
85
+ <div class="comparison-side yt-mockup light">
86
+ <span class="comparison-label">{t.labelLight}</span>
87
+ <div id="ytt-comparisonLight" class="mockup-grid"></div>
88
+ </div>
89
+ <div class="comparison-side yt-mockup dark">
90
+ <span class="comparison-label">{t.labelDark}</span>
91
+ <div id="ytt-comparisonDark" class="mockup-grid"></div>
92
+ </div>
93
+ </div>
94
+ </main>
95
+
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <script>
101
+ const VIEWS = [
102
+ '1.2M de visualizaciones • hace 1 hora',
103
+ 'Hace 45 minutos',
104
+ '250.000 visualizaciones • hace 1 día',
105
+ '3.1M de visualizaciones • hace 1 semana',
106
+ ];
107
+
108
+ let thumbnailsList: string[] = [];
109
+ let currentAvatar: string | null = null;
110
+ let currentView = 'desktop';
111
+ let galleryEl: HTMLElement | null = null;
112
+ let lightEl: HTMLElement | null = null;
113
+ let darkEl: HTMLElement | null = null;
114
+ let containerEl: HTMLElement | null = null;
115
+ let titleEl: HTMLInputElement | null = null;
116
+ let channelEl: HTMLInputElement | null = null;
117
+
118
+ function avatarHtml(): string {
119
+ return currentAvatar
120
+ ? `<img src="${currentAvatar}" alt="Avatar" />`
121
+ : `<svg viewBox="0 0 24 24" fill="currentColor" width="100%" height="100%"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>`;
122
+ }
123
+
124
+ function createVideoMock(thumb: string, i: number): string {
125
+ const title = titleEl?.value || 'Título del vídeo';
126
+ const channel = channelEl?.value || 'Canal';
127
+ return `<div class="yt-card">
128
+ <div class="yt-thumbnail-wrapper">
129
+ <img src="${thumb}" alt="Thumbnail" />
130
+ <div class="yt-duration">10:45</div>
131
+ </div>
132
+ <div class="yt-info-row">
133
+ <div class="yt-avatar">${avatarHtml()}</div>
134
+ <div class="yt-text-meta">
135
+ <h4 class="yt-title" title="${title}">${title}</h4>
136
+ <p class="yt-channel">${channel}</p>
137
+ <p class="yt-views">${VIEWS[i % 4]}</p>
138
+ </div>
139
+ </div>
140
+ </div>`;
141
+ }
142
+
143
+ function renderGallery(): void {
144
+ if (!galleryEl) return;
145
+ galleryEl.innerHTML = thumbnailsList.map((t, i) =>
146
+ `<div class="gallery-item"><img src="${t}" alt="Miniatura" /><button class="btn-remove-thumb" data-idx="${i}">&times;</button></div>`
147
+ ).join('');
148
+ galleryEl.querySelectorAll('.btn-remove-thumb').forEach((btn) => {
149
+ btn.addEventListener('click', (e) => {
150
+ const idx = parseInt((e.target as HTMLElement).dataset.idx || '0');
151
+ thumbnailsList.splice(idx, 1);
152
+ renderFeed();
153
+ });
154
+ });
155
+ }
156
+
157
+ function renderFeed(): void {
158
+ renderGallery();
159
+ if (containerEl) containerEl.className = `comparison-container mockup-${currentView}`;
160
+ const html = thumbnailsList.map((t, i) => createVideoMock(t, i)).join('');
161
+ if (lightEl) lightEl.innerHTML = html;
162
+ if (darkEl) darkEl.innerHTML = html;
163
+ }
164
+
165
+ function setupDropzone(el: HTMLElement | null, input: HTMLInputElement | null, handler: (files: FileList) => void): void {
166
+ if (!el || !input) return;
167
+ el.addEventListener('click', () => input.click());
168
+ el.addEventListener('dragover', (e) => { e.preventDefault(); el.classList.add('hover'); });
169
+ el.addEventListener('dragleave', () => el.classList.remove('hover'));
170
+ el.addEventListener('drop', (e) => {
171
+ e.preventDefault();
172
+ el.classList.remove('hover');
173
+ const files = (e as DragEvent).dataTransfer?.files;
174
+ if (files) handler(files);
175
+ });
176
+ input.addEventListener('change', (e) => {
177
+ const files = (e.target as HTMLInputElement).files;
178
+ if (files) handler(files);
179
+ });
180
+ }
181
+
182
+ function setupUploads(): void {
183
+ const thumbInput = document.getElementById('ytt-thumbInput') as HTMLInputElement | null;
184
+ const avatarInput = document.getElementById('ytt-avatarInput') as HTMLInputElement | null;
185
+ const dropThumb = document.getElementById('ytt-dropThumb') as HTMLElement | null;
186
+ const dropAvatar = document.getElementById('ytt-dropAvatar') as HTMLElement | null;
187
+ setupDropzone(dropThumb, thumbInput, (files) => {
188
+ for (const file of Array.from(files)) {
189
+ if (file.type.startsWith('image/')) thumbnailsList.push(URL.createObjectURL(file));
190
+ }
191
+ renderFeed();
192
+ });
193
+ setupDropzone(dropAvatar, avatarInput, (files) => {
194
+ const file = files[0];
195
+ if (file && file.type.startsWith('image/')) { currentAvatar = URL.createObjectURL(file); renderFeed(); }
196
+ });
197
+ }
198
+
199
+ function setupControls(): void {
200
+ const btnDesktop = document.getElementById('ytt-btnDesktop') as HTMLButtonElement | null;
201
+ const btnMobile = document.getElementById('ytt-btnMobile') as HTMLButtonElement | null;
202
+ const btnReset = document.getElementById('ytt-btnReset') as HTMLButtonElement | null;
203
+ btnDesktop?.addEventListener('click', () => {
204
+ currentView = 'desktop'; btnDesktop.classList.add('active'); btnMobile?.classList.remove('active'); renderFeed();
205
+ });
206
+ btnMobile?.addEventListener('click', () => {
207
+ currentView = 'mobile'; btnMobile.classList.add('active'); btnDesktop?.classList.remove('active'); renderFeed();
208
+ });
209
+ btnReset?.addEventListener('click', () => { thumbnailsList = []; renderFeed(); });
210
+ [titleEl, channelEl].forEach((inp) => inp?.addEventListener('input', renderFeed));
211
+ }
212
+
213
+ function init(): void {
214
+ if (!document.querySelector('.ytt-root')) return;
215
+ thumbnailsList = [];
216
+ currentAvatar = null;
217
+ currentView = 'desktop';
218
+ galleryEl = document.getElementById('ytt-galleryPreview');
219
+ lightEl = document.getElementById('ytt-comparisonLight');
220
+ darkEl = document.getElementById('ytt-comparisonDark');
221
+ containerEl = document.getElementById('ytt-comparisonContainer');
222
+ titleEl = document.getElementById('ytt-mockupTitle') as HTMLInputElement | null;
223
+ channelEl = document.getElementById('ytt-mockupChannel') as HTMLInputElement | null;
224
+ setupUploads();
225
+ setupControls();
226
+ renderFeed();
227
+ }
228
+
229
+ document.addEventListener('astro:page-load', init);
230
+ init();
231
+ </script>
232
+
233
+ <style>
234
+ .ytt-root {
235
+ --yt-red: #f00;
236
+
237
+ max-width: 1400px;
238
+ margin: 0 auto;
239
+ }
240
+
241
+ .premium-card {
242
+ background: rgba(255, 255, 255, 0.8);
243
+ backdrop-filter: blur(20px);
244
+ -webkit-backdrop-filter: blur(20px);
245
+ border: 1px solid rgba(255, 255, 255, 0.4);
246
+ border-radius: 32px;
247
+ padding: 2rem;
248
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.05);
249
+ }
250
+
251
+ @media (max-width: 768px) {
252
+ .premium-card {
253
+ padding: 1rem;
254
+ border-radius: 20px;
255
+ }
256
+ }
257
+
258
+ :global(.theme-dark) .ytt-root .premium-card {
259
+ background: rgba(15, 23, 42, 0.7);
260
+ border-color: rgba(255, 255, 255, 0.05);
261
+ }
262
+
263
+ .main-layout {
264
+ display: grid;
265
+ grid-template-columns: 320px 1fr;
266
+ gap: 2rem;
267
+ align-items: start;
268
+ }
269
+
270
+ @media (max-width: 1100px) { .main-layout { grid-template-columns: 1fr; } }
271
+
272
+ @media (max-width: 768px) { .main-layout { gap: 1rem; } }
273
+
274
+ .sidebar {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 1.5rem;
278
+ position: sticky;
279
+ top: 2rem;
280
+ }
281
+
282
+ @media (max-width: 1100px) { .sidebar { position: static; } }
283
+
284
+ @media (max-width: 768px) { .sidebar { gap: 1rem; } }
285
+
286
+ .upload-group {
287
+ display: grid;
288
+ grid-template-columns: 1fr 1fr;
289
+ gap: 0.75rem;
290
+ }
291
+
292
+ @media (max-width: 768px) {
293
+ .upload-group {
294
+ grid-template-columns: 1fr;
295
+ gap: 0.5rem;
296
+ }
297
+ }
298
+
299
+ .drop-zone {
300
+ width: 100%;
301
+ height: 100px;
302
+ border: 2px dashed rgba(239, 68, 68, 0.15);
303
+ border-radius: 20px;
304
+ display: flex;
305
+ flex-direction: column;
306
+ align-items: center;
307
+ justify-content: center;
308
+ gap: 0.4rem;
309
+ cursor: pointer;
310
+ background: rgba(239, 68, 68, 0.02);
311
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
312
+ text-align: center;
313
+ padding: 0.5rem;
314
+ color: inherit;
315
+ }
316
+
317
+ @media (max-width: 768px) {
318
+ .drop-zone {
319
+ height: 80px;
320
+ border-radius: 16px;
321
+ }
322
+ }
323
+
324
+ .drop-zone:hover, .drop-zone.hover {
325
+ border-color: var(--yt-red);
326
+ background: rgba(239, 68, 68, 0.06);
327
+ transform: translateY(-2px);
328
+ }
329
+
330
+ .drop-icon {
331
+ font-size: 1.6rem;
332
+ color: var(--yt-red);
333
+ }
334
+
335
+ .drop-text h3 {
336
+ margin: 0;
337
+ font-size: 0.8rem;
338
+ font-weight: 800;
339
+ }
340
+
341
+ .drop-text p {
342
+ margin: 0;
343
+ font-size: 0.6rem;
344
+ color: #94a3b8;
345
+ }
346
+
347
+ @media (max-width: 768px) {
348
+ .drop-text h3 { font-size: 0.7rem; }
349
+ .drop-text p { font-size: 0.55rem; }
350
+ .drop-icon { font-size: 1.2rem; }
351
+ }
352
+
353
+ .gallery-preview {
354
+ display: flex;
355
+ flex-wrap: wrap;
356
+ gap: 8px;
357
+ padding: 12px;
358
+ background: rgba(0, 0, 0, 0.03);
359
+ border: 1px solid rgba(0, 0, 0, 0.05);
360
+ border-radius: 16px;
361
+ min-height: 50px;
362
+ }
363
+
364
+ @media (max-width: 768px) {
365
+ .gallery-preview {
366
+ gap: 6px;
367
+ padding: 8px;
368
+ border-radius: 12px;
369
+ }
370
+ }
371
+
372
+ :global(.theme-dark) .ytt-root .gallery-preview {
373
+ background: rgba(255, 255, 255, 0.03);
374
+ border-color: rgba(255, 255, 255, 0.06);
375
+ }
376
+
377
+ .custom-settings {
378
+ display: flex;
379
+ flex-direction: column;
380
+ gap: 1rem;
381
+ padding: 1.25rem;
382
+ background: rgba(0, 0, 0, 0.03);
383
+ border-radius: 20px;
384
+ border: 1px solid rgba(0, 0, 0, 0.05);
385
+ }
386
+
387
+ @media (max-width: 768px) {
388
+ .custom-settings {
389
+ padding: 1rem;
390
+ gap: 0.75rem;
391
+ border-radius: 16px;
392
+ }
393
+ }
394
+
395
+ :global(.theme-dark) .ytt-root .custom-settings {
396
+ background: rgba(255, 255, 255, 0.03);
397
+ border-color: rgba(255, 255, 255, 0.06);
398
+ }
399
+
400
+ .settings-group label {
401
+ display: block;
402
+ font-size: 0.7rem;
403
+ font-weight: 800;
404
+ color: #94a3b8;
405
+ text-transform: uppercase;
406
+ letter-spacing: 0.05em;
407
+ margin-bottom: 0.3rem;
408
+ }
409
+
410
+ .settings-group input {
411
+ width: 100%;
412
+ padding: 0.8rem;
413
+ border-radius: 12px;
414
+ border: 1px solid rgba(0, 0, 0, 0.1);
415
+ font-size: 0.9rem;
416
+ background: white;
417
+ color: #1e293b;
418
+ outline: none;
419
+ transition: all 0.2s;
420
+ box-sizing: border-box;
421
+ }
422
+
423
+ @media (max-width: 768px) {
424
+ .settings-group label {
425
+ font-size: 0.65rem;
426
+ }
427
+
428
+ .settings-group input {
429
+ padding: 0.7rem;
430
+ font-size: 0.85rem;
431
+ border-radius: 10px;
432
+ }
433
+ }
434
+
435
+ :global(.theme-dark) .ytt-root .settings-group input {
436
+ background: rgba(15, 23, 42, 0.5);
437
+ border-color: rgba(255, 255, 255, 0.1);
438
+ color: #f1f5f9;
439
+ }
440
+
441
+ .settings-group input:focus {
442
+ border-color: var(--yt-red);
443
+ box-shadow: 0 0 0 4px rgba(255, 0, 0, 0.05);
444
+ }
445
+
446
+ .tester-controls {
447
+ display: grid;
448
+ grid-template-columns: repeat(2, 1fr);
449
+ gap: 0.75rem;
450
+ }
451
+
452
+ @media (max-width: 768px) {
453
+ .tester-controls {
454
+ grid-template-columns: 1fr;
455
+ gap: 0.5rem;
456
+ }
457
+ }
458
+
459
+ .control-btn {
460
+ border: none;
461
+ background: rgba(0, 0, 0, 0.05);
462
+ padding: 1rem;
463
+ border-radius: 14px;
464
+ cursor: pointer;
465
+ font-weight: 700;
466
+ font-size: 0.8rem;
467
+ display: flex;
468
+ align-items: center;
469
+ justify-content: center;
470
+ gap: 0.5rem;
471
+ color: #64748b;
472
+ transition: all 0.2s;
473
+ }
474
+
475
+ @media (max-width: 768px) {
476
+ .control-btn {
477
+ padding: 0.8rem;
478
+ font-size: 0.75rem;
479
+ border-radius: 12px;
480
+ }
481
+ }
482
+
483
+ .control-btn:hover {
484
+ background: rgba(0, 0, 0, 0.08);
485
+ color: #1e293b;
486
+ }
487
+
488
+ :global(.theme-dark) .ytt-root .control-btn {
489
+ background: rgba(255, 255, 255, 0.05);
490
+ color: #94a3b8;
491
+ }
492
+ :global(.theme-dark) .ytt-root .control-btn:hover { color: #f1f5f9; }
493
+
494
+ .control-btn.active {
495
+ background: var(--yt-red);
496
+ color: white;
497
+ box-shadow: 0 8px 20px rgba(255, 0, 0, 0.15);
498
+ }
499
+
500
+ .control-btn--reset {
501
+ grid-column: span 2;
502
+ color: #ef4444;
503
+ border: 1px solid rgba(239, 68, 68, 0.2);
504
+ }
505
+
506
+ @media (max-width: 768px) {
507
+ .control-btn--reset {
508
+ grid-column: span 1;
509
+ }
510
+ }
511
+
512
+ .preview-area {
513
+ min-width: 0;
514
+ }
515
+ </style>
516
+
517
+ <style>
518
+ :global(.comparison-container) {
519
+ display: grid;
520
+ grid-template-columns: 1fr 1fr;
521
+ gap: 4px;
522
+ background: #e2e8f0;
523
+ border-radius: 32px;
524
+ overflow: hidden;
525
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.05);
526
+ }
527
+
528
+ :global(.theme-dark) :global(.comparison-container) {
529
+ background: rgba(255, 255, 255, 0.05);
530
+ }
531
+
532
+ @media (max-width: 768px) {
533
+ :global(.comparison-container) {
534
+ grid-template-columns: 1fr;
535
+ border-radius: 20px;
536
+ gap: 8px;
537
+ }
538
+ }
539
+
540
+ :global(.comparison-side) {
541
+ position: relative;
542
+ padding: 3rem 2rem;
543
+ display: flex;
544
+ flex-direction: column;
545
+ align-items: center;
546
+ min-height: 600px;
547
+ transition: background 0.3s ease;
548
+ }
549
+
550
+ @media (max-width: 768px) {
551
+ :global(.comparison-side) {
552
+ padding: 2rem 1rem;
553
+ min-height: auto;
554
+ }
555
+ }
556
+
557
+ :global(.comparison-label) {
558
+ position: absolute;
559
+ top: 1.25rem;
560
+ left: 1.25rem;
561
+ background: rgba(0, 0, 0, 0.6);
562
+ color: white;
563
+ padding: 6px 12px;
564
+ border-radius: 8px;
565
+ font-size: 11px;
566
+ font-weight: 900;
567
+ z-index: 20;
568
+ backdrop-filter: blur(8px);
569
+ border: 1px solid rgba(255, 255, 255, 0.1);
570
+ }
571
+
572
+ @media (max-width: 768px) {
573
+ :global(.comparison-label) {
574
+ font-size: 10px;
575
+ padding: 4px 8px;
576
+ top: 0.75rem;
577
+ left: 0.75rem;
578
+ }
579
+ }
580
+
581
+ :global(.mockup-grid) {
582
+ display: grid;
583
+ gap: 3rem;
584
+ width: 100%;
585
+ }
586
+
587
+ :global(.mockup-desktop .mockup-grid) {
588
+ max-width: 520px;
589
+ }
590
+
591
+ :global(.mockup-mobile .mockup-grid) {
592
+ max-width: 320px;
593
+ }
594
+
595
+ @media (max-width: 768px) {
596
+ :global(.mockup-grid) { gap: 2rem; }
597
+ :global(.mockup-desktop .mockup-grid),
598
+ :global(.mockup-mobile .mockup-grid) { max-width: 100%; }
599
+ }
600
+
601
+ :global(.yt-card) {
602
+ width: 100%;
603
+ display: flex;
604
+ flex-direction: column;
605
+ gap: 1rem;
606
+ animation: ytt-slide-up 0.4s ease-out backwards;
607
+ }
608
+
609
+ @media (max-width: 768px) {
610
+ :global(.yt-card) {
611
+ gap: 0.75rem;
612
+ }
613
+ }
614
+
615
+ @keyframes ytt-slide-up {
616
+ from {
617
+ opacity: 0;
618
+ transform: translateY(20px);
619
+ }
620
+
621
+ to {
622
+ opacity: 1;
623
+ transform: translateY(0);
624
+ }
625
+ }
626
+
627
+ :global(.yt-thumbnail-wrapper) {
628
+ position: relative;
629
+ width: 100%;
630
+ aspect-ratio: 16 / 9;
631
+ border-radius: 16px;
632
+ overflow: hidden;
633
+ background: #e2e8f0;
634
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
635
+ }
636
+
637
+ :global(.yt-thumbnail-wrapper img) {
638
+ width: 100%;
639
+ height: 100%;
640
+ object-fit: cover;
641
+ }
642
+
643
+ :global(.yt-duration) {
644
+ position: absolute;
645
+ bottom: 10px;
646
+ right: 10px;
647
+ background: rgba(0, 0, 0, 0.9);
648
+ color: white;
649
+ padding: 3px 6px;
650
+ border-radius: 5px;
651
+ font-size: 12px;
652
+ font-weight: 600;
653
+ }
654
+
655
+ @media (max-width: 768px) {
656
+ :global(.yt-duration) {
657
+ font-size: 10px;
658
+ padding: 2px 4px;
659
+ bottom: 6px;
660
+ right: 6px;
661
+ }
662
+ }
663
+
664
+ :global(.yt-info-row) {
665
+ display: flex;
666
+ gap: 14px;
667
+ }
668
+
669
+ @media (max-width: 768px) {
670
+ :global(.yt-info-row) {
671
+ gap: 10px;
672
+ }
673
+ }
674
+
675
+ :global(.yt-avatar) {
676
+ width: 44px;
677
+ height: 44px;
678
+ border-radius: 50%;
679
+ flex-shrink: 0;
680
+ overflow: hidden;
681
+ background: #e5e7eb;
682
+ border: 1px solid rgba(0, 0, 0, 0.05);
683
+ display: flex;
684
+ align-items: center;
685
+ justify-content: center;
686
+ color: #94a3b8;
687
+ }
688
+
689
+ :global(.mockup-mobile .yt-avatar) {
690
+ width: 36px;
691
+ height: 36px;
692
+ }
693
+
694
+ :global(.yt-avatar img) {
695
+ width: 100%;
696
+ height: 100%;
697
+ object-fit: cover;
698
+ }
699
+
700
+ :global(.yt-avatar svg) {
701
+ width: 60%;
702
+ height: 60%;
703
+ }
704
+
705
+ :global(.yt-text-meta) {
706
+ display: flex;
707
+ flex-direction: column;
708
+ gap: 4px;
709
+ min-width: 0;
710
+ }
711
+
712
+ :global(.yt-title) {
713
+ font-weight: 700;
714
+ margin: 0;
715
+ line-height: 1.4;
716
+ display: -webkit-box;
717
+ -webkit-line-clamp: 2;
718
+ -webkit-box-orient: vertical;
719
+ overflow: hidden;
720
+ color: inherit;
721
+ }
722
+
723
+ :global(.mockup-desktop .yt-title) { font-size: 1.1rem; }
724
+ :global(.mockup-mobile .yt-title) { font-size: 0.9rem; }
725
+
726
+ :global(.yt-channel), :global(.yt-views) {
727
+ font-size: 0.85rem;
728
+ margin: 0;
729
+ font-weight: 500;
730
+ }
731
+
732
+ :global(.mockup-mobile .yt-channel), :global(.mockup-mobile .yt-views) {
733
+ font-size: 0.75rem;
734
+ }
735
+
736
+ @media (max-width: 768px) {
737
+ :global(.yt-channel), :global(.yt-views) { font-size: 0.75rem; }
738
+ :global(.mockup-mobile .yt-channel), :global(.mockup-mobile .yt-views) { font-size: 0.65rem; }
739
+ }
740
+
741
+ :global(.light .yt-channel), :global(.light .yt-views) { color: #606060; }
742
+ :global(.dark .yt-channel), :global(.dark .yt-views) { color: #aaa; }
743
+
744
+ :global(.yt-mockup.light) {
745
+ background: #f9f9f9;
746
+ color: #030303;
747
+ }
748
+
749
+ :global(.yt-mockup.dark) {
750
+ background: #0f0f0f;
751
+ color: #f1f1f1;
752
+ }
753
+
754
+ :global(.gallery-item) {
755
+ width: 60px;
756
+ aspect-ratio: 16/9;
757
+ border-radius: 6px;
758
+ overflow: hidden;
759
+ position: relative;
760
+ border: 1px solid rgba(0, 0, 0, 0.1);
761
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
762
+ }
763
+
764
+ :global(.gallery-item img) {
765
+ width: 100%;
766
+ height: 100%;
767
+ object-fit: cover;
768
+ }
769
+
770
+ :global(.btn-remove-thumb) {
771
+ position: absolute;
772
+ top: -4px;
773
+ right: -4px;
774
+ background: #ef4444;
775
+ color: white;
776
+ border: none;
777
+ border-radius: 50%;
778
+ width: 18px;
779
+ height: 18px;
780
+ display: flex;
781
+ align-items: center;
782
+ justify-content: center;
783
+ font-size: 11px;
784
+ cursor: pointer;
785
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
786
+ opacity: 0;
787
+ transition: opacity 0.2s;
788
+ }
789
+
790
+ :global(.gallery-item:hover .btn-remove-thumb) { opacity: 1; }
791
+ </style>