@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,857 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { SafeZonesUI } 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 SafeZonesUI;
12
+
13
+ const TIKTOK_SVG = `<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.28 0 .54.04.79.1V9.01a6.34 6.34 0 0 0-.79-.05 6.34 6.34 0 0 0-6.34 6.34 6.34 6.34 0 0 0 6.34 6.34 6.34 6.34 0 0 0 6.33-6.34V9.22a8.22 8.22 0 0 0 4.83 1.56V7.33a4.85 4.85 0 0 1-1.06-.64z"/></svg>`;
14
+ const INSTAGRAM_SVG = `<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z"/></svg>`;
15
+ const YOUTUBE_SVG = `<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>`;
16
+ ---
17
+
18
+ <script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js" is:inline></script>
19
+
20
+ <div
21
+ class="szl-root"
22
+ data-banner-tiktok={t.bannerTiktok}
23
+ data-banner-reels={t.bannerReels}
24
+ data-banner-shorts={t.bannerShorts}
25
+ data-load-content={t.loadContent}
26
+ data-upload-link={t.uploadLink}
27
+ data-tt-following={t.ttFollowing}
28
+ data-tt-for-you={t.ttForYou}
29
+ data-tt-description={t.ttDescription}
30
+ data-tt-audio={t.ttAudio}
31
+ data-tt-nav-home={t.ttNavHome}
32
+ data-tt-nav-friends={t.ttNavFriends}
33
+ data-tt-nav-inbox={t.ttNavInbox}
34
+ data-tt-nav-profile={t.ttNavProfile}
35
+ data-reels-follow={t.reelsFollow}
36
+ data-reels-description={t.reelsDescription}
37
+ data-reels-audio={t.reelsAudio}
38
+ data-shorts-subscribe={t.shortsSubscribe}
39
+ data-shorts-dislike={t.shortsDislike}
40
+ data-shorts-share={t.shortsShare}
41
+ data-shorts-remix={t.shortsRemix}
42
+ data-shorts-description={t.shortsDescription}
43
+ >
44
+ <div class="szl-grid">
45
+
46
+ <div class="szl-panel-left">
47
+ <div class="szl-card">
48
+ <h2 class="szl-card-title">
49
+ <div class="szl-card-icon">
50
+ <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.92c.04-.34.07-.69.07-1.08 0-.39-.03-.74-.07-1.08l2.32-1.81c.21-.16.27-.45.14-.69l-2.2-3.81c-.13-.24-.42-.32-.65-.24l-2.73 1.1c-.57-.44-1.17-.81-1.84-1.08L14 2.42C13.96 2.18 13.74 2 13.5 2h-3c-.24 0-.46.18-.49.42l-.41 2.47c-.67.27-1.27.64-1.84 1.08l-2.73-1.1c-.23-.08-.52 0-.65.24L2.18 9.11c-.13.24-.07.53.14.69l2.32 1.81C4.6 11.96 4.5 12.32 4.5 12.5c0 .18.1.54.14.9l-2.32 1.81c-.21.16-.27.45-.14.69l2.2 3.81c.13.24.42.32.65.24l2.73-1.1c.57.44 1.17.81 1.84 1.08l.41 2.47c.03.24.25.42.49.42h3c.24 0 .46-.18.49-.42l.41-2.47c.67-.27 1.27-.64 1.84-1.08l2.73 1.1c.23.08.52 0 .65-.24l2.2-3.81c.13-.24.07-.53-.14-.69l-2.32-1.81z"/></svg>
51
+ </div>
52
+ {t.labelConfig}
53
+ </h2>
54
+
55
+ <div class="szl-section">
56
+ <span class="szl-section-label">{t.labelPlatform}</span>
57
+ <div class="szl-platform-grid">
58
+ <button class="szl-platform-btn" data-szl-platform-btn data-platform="tiktok">
59
+ <span class="szl-platform-icon" set:html={TIKTOK_SVG} />
60
+ <span class="szl-platform-name">{t.nameTiktok}</span>
61
+ </button>
62
+ <button class="szl-platform-btn" data-szl-platform-btn data-platform="reels">
63
+ <span class="szl-platform-icon" set:html={INSTAGRAM_SVG} />
64
+ <span class="szl-platform-name">{t.nameReels}</span>
65
+ </button>
66
+ <button class="szl-platform-btn" data-szl-platform-btn data-platform="shorts">
67
+ <span class="szl-platform-icon" set:html={YOUTUBE_SVG} />
68
+ <span class="szl-platform-name">{t.nameShorts}</span>
69
+ </button>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="szl-section">
74
+ <span class="szl-section-label">{t.labelUpload}</span>
75
+ <label class="szl-upload-zone" data-szl-upload-zone>
76
+ <div class="szl-upload-inner">
77
+ <svg viewBox="0 0 24 24" fill="currentColor" width="40" height="40" class="szl-upload-icon"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zM8 15.01l1.41 1.41L11 14.84V19h2v-4.16l1.59 1.59L16 15.01 12.01 11z"/></svg>
78
+ <p class="szl-upload-text">{t.uploadPrompt}</p>
79
+ </div>
80
+ <input type="file" class="szl-file-hidden" data-szl-file accept=".jpg,.jpeg,.png,.webp,.gif,.mp4,.mov,.webm,.avi" />
81
+ </label>
82
+ </div>
83
+
84
+ <div class="szl-section szl-section-border">
85
+ <div class="szl-opacity-header">
86
+ <span class="szl-section-label">{t.labelOpacity}</span>
87
+ <span class="szl-opacity-val" data-szl-opacity-val>85%</span>
88
+ </div>
89
+ <input type="range" class="szl-slider" data-szl-slider min="0" max="100" value="85" />
90
+ </div>
91
+
92
+ <div class="szl-toggles">
93
+ <label class="szl-toggle-item">
94
+ <input type="checkbox" class="szl-checkbox" data-szl-mask />
95
+ <span class="szl-toggle-label">{t.labelMask}</span>
96
+ </label>
97
+ <label class="szl-toggle-item">
98
+ <input type="checkbox" class="szl-checkbox" data-szl-grid />
99
+ <span class="szl-toggle-label">{t.labelGrid}</span>
100
+ </label>
101
+ </div>
102
+ </div>
103
+
104
+ <button class="szl-reset-btn" data-szl-reset>
105
+ <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
106
+ {t.btnReset}
107
+ </button>
108
+ </div>
109
+
110
+ <div class="szl-preview-area">
111
+ <div class="szl-radial-bg"></div>
112
+ <div class="szl-phone" data-szl-phone>
113
+
114
+ <div class="szl-statusbar">
115
+ <span data-szl-clock>11:51</span>
116
+ <div class="szl-notch"></div>
117
+ <div class="szl-statusbar-icons">
118
+ <iconify-icon icon="mdi:signal" style="font-size:12px"></iconify-icon>
119
+ <iconify-icon icon="mdi:wifi" style="font-size:12px"></iconify-icon>
120
+ <iconify-icon icon="mdi:battery-90" style="font-size:12px"></iconify-icon>
121
+ </div>
122
+ </div>
123
+
124
+ <div class="szl-media-wrapper" data-szl-media-wrapper>
125
+ <div class="szl-empty-state" data-szl-empty>
126
+ <iconify-icon icon="mdi:camera-plus-outline" style="font-size:72px;display:block;margin-bottom:16px"></iconify-icon>
127
+ <p class="szl-empty-text">{t.loadContent}</p>
128
+ <p class="szl-empty-link" data-szl-upload-link>{t.uploadLink}</p>
129
+ </div>
130
+ <div class="szl-media" data-szl-media></div>
131
+ </div>
132
+
133
+ <div class="szl-overlay" data-szl-overlay></div>
134
+
135
+ <div class="szl-mask szl-mask-hidden" data-szl-mask-overlay>
136
+ <div class="szl-mask-top"></div>
137
+ <div class="szl-mask-flex"></div>
138
+ <div class="szl-mask-bottom"></div>
139
+ <div class="szl-mask-right"></div>
140
+ </div>
141
+
142
+ <div class="szl-grid-overlay szl-grid-hidden" data-szl-grid-overlay>
143
+ <div class="szl-grid-inner">
144
+ <div class="szl-grid-h szl-grid-h1"></div>
145
+ <div class="szl-grid-h szl-grid-h2"></div>
146
+ <div class="szl-grid-v szl-grid-v1"></div>
147
+ <div class="szl-grid-v szl-grid-v2"></div>
148
+ </div>
149
+ </div>
150
+
151
+ </div>
152
+
153
+ <div class="szl-banner" data-szl-banner>{t.bannerTiktok}</div>
154
+ </div>
155
+
156
+ </div>
157
+ </div>
158
+
159
+ <script>
160
+ import { buildPlatformUIs } from './logic';
161
+ import type { PlatformStrings } from './logic';
162
+
163
+ let clockInterval: number | null = null;
164
+
165
+ function ds(val: string | undefined): string {
166
+ return val !== undefined ? val : '';
167
+ }
168
+
169
+ function getTikTokStrings(d: DOMStringMap) {
170
+ return {
171
+ ttFollowing: ds(d.ttFollowing),
172
+ ttForYou: ds(d.ttForYou),
173
+ ttDescription: ds(d.ttDescription),
174
+ ttAudio: ds(d.ttAudio),
175
+ ttNavHome: ds(d.ttNavHome),
176
+ ttNavFriends: ds(d.ttNavFriends),
177
+ ttNavInbox: ds(d.ttNavInbox),
178
+ ttNavProfile: ds(d.ttNavProfile),
179
+ };
180
+ }
181
+
182
+ function getReelsStrings(d: DOMStringMap) {
183
+ return {
184
+ reelsFollow: d.reelsFollow ?? '',
185
+ reelsDescription: d.reelsDescription ?? '',
186
+ reelsAudio: d.reelsAudio ?? '',
187
+ };
188
+ }
189
+
190
+ function getShortsStrings(d: DOMStringMap) {
191
+ return {
192
+ shortsSubscribe: d.shortsSubscribe ?? '',
193
+ shortsDislike: d.shortsDislike ?? '',
194
+ shortsShare: d.shortsShare ?? '',
195
+ shortsRemix: d.shortsRemix ?? '',
196
+ shortsDescription: d.shortsDescription ?? '',
197
+ };
198
+ }
199
+
200
+ function getPlatformStrings(container: HTMLElement): PlatformStrings {
201
+ const d = container.dataset;
202
+ return {
203
+ ...getTikTokStrings(d),
204
+ ...getReelsStrings(d),
205
+ ...getShortsStrings(d),
206
+ };
207
+ }
208
+
209
+ function updateClock(clockEl: HTMLElement) {
210
+ clockEl.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
211
+ }
212
+
213
+ function updateBanner(container: HTMLElement, platform: string) {
214
+ const banner = container.querySelector<HTMLElement>('[data-szl-banner]');
215
+ if (!banner) return;
216
+ const key = `banner${platform.charAt(0).toUpperCase()}${platform.slice(1)}` as keyof typeof container.dataset;
217
+ banner.textContent = container.dataset[key] ?? platform;
218
+ }
219
+
220
+ function updateOverlay(container: HTMLElement) {
221
+ const overlay = container.querySelector<HTMLElement>('[data-szl-overlay]');
222
+ if (!overlay) return;
223
+ const platform = container.dataset.platform ?? 'tiktok';
224
+ const slider = container.querySelector<HTMLInputElement>('[data-szl-slider]');
225
+ const opacity = slider ? parseInt(slider.value) / 100 : 0.85;
226
+ const uis = buildPlatformUIs(getPlatformStrings(container));
227
+ overlay.innerHTML = uis[platform] ?? uis['tiktok'];
228
+ overlay.style.opacity = String(opacity);
229
+ }
230
+
231
+ function activatePlatformBtns(container: HTMLElement, platform: string) {
232
+ container.querySelectorAll('[data-szl-platform-btn]').forEach((btn) => {
233
+ btn.classList.toggle('szl-btn-active', btn.getAttribute('data-platform') === platform);
234
+ });
235
+ }
236
+
237
+ function handlePlatformClick(container: HTMLElement, btn: Element) {
238
+ const platform = btn.getAttribute('data-platform') ?? 'tiktok';
239
+ container.dataset.platform = platform;
240
+ activatePlatformBtns(container, platform);
241
+ updateOverlay(container);
242
+ updateBanner(container, platform);
243
+ }
244
+
245
+ function renderMedia(display: HTMLElement, emptyState: HTMLElement, file: File) {
246
+ const reader = new FileReader();
247
+ reader.onload = (e) => {
248
+ const src = e.target?.result as string;
249
+ display.innerHTML = file.type.startsWith('video/')
250
+ ? `<video src="${src}" autoplay loop muted style="width:100%;height:100%;object-fit:cover"></video>`
251
+ : `<img src="${src}" style="width:100%;height:100%;object-fit:cover" alt="" />`;
252
+ emptyState.classList.add('szl-hidden');
253
+ display.classList.remove('szl-hidden');
254
+ };
255
+ reader.readAsDataURL(file);
256
+ }
257
+
258
+ function resetMedia(container: HTMLElement) {
259
+ const display = container.querySelector<HTMLElement>('[data-szl-media]');
260
+ const emptyState = container.querySelector<HTMLElement>('[data-szl-empty]');
261
+ const fileInput = container.querySelector<HTMLInputElement>('[data-szl-file]');
262
+ if (display) { display.innerHTML = ''; display.classList.add('szl-hidden'); }
263
+ emptyState?.classList.remove('szl-hidden');
264
+ if (fileInput) fileInput.value = '';
265
+ }
266
+
267
+ function attachPlatformListeners(container: HTMLElement) {
268
+ container.querySelectorAll('[data-szl-platform-btn]').forEach((btn) => {
269
+ btn.addEventListener('click', () => handlePlatformClick(container, btn));
270
+ });
271
+ }
272
+
273
+ function attachFileListener(container: HTMLElement) {
274
+ const fileInput = container.querySelector<HTMLInputElement>('[data-szl-file]');
275
+ const display = container.querySelector<HTMLElement>('[data-szl-media]');
276
+ const emptyState = container.querySelector<HTMLElement>('[data-szl-empty]');
277
+ fileInput?.addEventListener('change', (e) => {
278
+ const file = (e.target as HTMLInputElement).files?.[0];
279
+ if (!file || !display || !emptyState) return;
280
+ renderMedia(display, emptyState, file);
281
+ });
282
+ container.querySelector('[data-szl-upload-link]')?.addEventListener('click', () => {
283
+ fileInput?.click();
284
+ });
285
+ }
286
+
287
+ function attachSliderListener(container: HTMLElement) {
288
+ const slider = container.querySelector<HTMLInputElement>('[data-szl-slider]');
289
+ const valEl = container.querySelector<HTMLElement>('[data-szl-opacity-val]');
290
+ slider?.addEventListener('input', () => {
291
+ if (valEl) valEl.textContent = `${slider.value}%`;
292
+ updateOverlay(container);
293
+ });
294
+ }
295
+
296
+ function attachToggleListeners(container: HTMLElement) {
297
+ const maskToggle = container.querySelector<HTMLInputElement>('[data-szl-mask]');
298
+ const gridToggle = container.querySelector<HTMLInputElement>('[data-szl-grid]');
299
+ const maskOverlay = container.querySelector<HTMLElement>('[data-szl-mask-overlay]');
300
+ const gridOverlay = container.querySelector<HTMLElement>('[data-szl-grid-overlay]');
301
+ maskToggle?.addEventListener('change', () => {
302
+ maskOverlay?.classList.toggle('szl-mask-hidden', !maskToggle.checked);
303
+ });
304
+ gridToggle?.addEventListener('change', () => {
305
+ gridOverlay?.classList.toggle('szl-grid-hidden', !gridToggle.checked);
306
+ });
307
+ }
308
+
309
+ function attachResetListener(container: HTMLElement) {
310
+ container.querySelector('[data-szl-reset]')?.addEventListener('click', () => resetMedia(container));
311
+ }
312
+
313
+ function attachListeners(container: HTMLElement) {
314
+ container.dataset.platform = 'tiktok';
315
+ const clockEl = container.querySelector<HTMLElement>('[data-szl-clock]');
316
+ if (clockEl) {
317
+ if (clockInterval) clearInterval(clockInterval);
318
+ updateClock(clockEl);
319
+ clockInterval = window.setInterval(() => updateClock(clockEl), 1000);
320
+ }
321
+ attachPlatformListeners(container);
322
+ attachFileListener(container);
323
+ attachSliderListener(container);
324
+ attachToggleListeners(container);
325
+ attachResetListener(container);
326
+ const display = container.querySelector<HTMLElement>('[data-szl-media]');
327
+ display?.classList.add('szl-hidden');
328
+ activatePlatformBtns(container, 'tiktok');
329
+ updateOverlay(container);
330
+ updateBanner(container, 'tiktok');
331
+ }
332
+
333
+ function init() {
334
+ document.querySelectorAll<HTMLElement>('.szl-root').forEach((c) => attachListeners(c));
335
+ }
336
+
337
+ document.addEventListener('astro:before-swap', () => {
338
+ if (clockInterval) { clearInterval(clockInterval); clockInterval = null; }
339
+ });
340
+ document.addEventListener('astro:page-load', init);
341
+ init();
342
+ </script>
343
+
344
+ <style>
345
+ .szl-root {
346
+ max-width: 1200px;
347
+ margin: 0 auto;
348
+ padding: 2rem 1rem;
349
+ }
350
+
351
+ .szl-grid {
352
+ display: grid;
353
+ grid-template-columns: 1fr;
354
+ gap: 2.5rem;
355
+ align-items: start;
356
+ }
357
+
358
+ @media (min-width: 1024px) {
359
+ .szl-grid {
360
+ grid-template-columns: 340px 1fr;
361
+ }
362
+ }
363
+
364
+ .szl-panel-left {
365
+ display: flex;
366
+ flex-direction: column;
367
+ gap: 1.5rem;
368
+ }
369
+
370
+ .szl-card {
371
+ background: var(--bg-surface);
372
+ border: 1px solid var(--border-color);
373
+ border-radius: 2.5rem;
374
+ padding: 2rem;
375
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
376
+ display: flex;
377
+ flex-direction: column;
378
+ gap: 2rem;
379
+ }
380
+
381
+ .szl-card-title {
382
+ font-size: 1.1rem;
383
+ font-weight: 700;
384
+ color: var(--text-main);
385
+ display: flex;
386
+ align-items: center;
387
+ gap: 0.75rem;
388
+ margin: 0;
389
+ }
390
+
391
+ .szl-card-icon {
392
+ width: 2.5rem;
393
+ height: 2.5rem;
394
+ border-radius: 0.75rem;
395
+ background: rgba(99, 102, 241, 0.1);
396
+ display: flex;
397
+ align-items: center;
398
+ justify-content: center;
399
+ color: #6366f1;
400
+ flex-shrink: 0;
401
+ }
402
+
403
+ .szl-section {
404
+ display: flex;
405
+ flex-direction: column;
406
+ gap: 0.75rem;
407
+ }
408
+
409
+ .szl-section-border {
410
+ padding-top: 1.5rem;
411
+ border-top: 1px solid var(--border-color);
412
+ }
413
+
414
+ .szl-section-label {
415
+ font-size: 0.625rem;
416
+ font-weight: 700;
417
+ text-transform: uppercase;
418
+ letter-spacing: 0.1em;
419
+ color: var(--text-muted);
420
+ display: block;
421
+ }
422
+
423
+ .szl-platform-grid {
424
+ display: grid;
425
+ grid-template-columns: repeat(3, 1fr);
426
+ gap: 0.75rem;
427
+ }
428
+
429
+ .szl-platform-btn {
430
+ display: flex;
431
+ flex-direction: column;
432
+ align-items: center;
433
+ gap: 0.5rem;
434
+ padding: 0.75rem;
435
+ border-radius: 1rem;
436
+ border: 1px solid var(--border-color);
437
+ background: var(--bg-page);
438
+ cursor: pointer;
439
+ transition: all 0.2s ease;
440
+ color: var(--text-muted);
441
+ }
442
+
443
+ .szl-platform-btn:hover {
444
+ border-color: rgba(99, 102, 241, 0.5);
445
+ color: var(--text-main);
446
+ }
447
+
448
+ .szl-platform-btn.szl-btn-active {
449
+ border-color: #6366f1;
450
+ background: rgba(99, 102, 241, 0.1);
451
+ color: #6366f1;
452
+ }
453
+
454
+ .szl-platform-icon {
455
+ display: flex;
456
+ width: 24px;
457
+ height: 24px;
458
+ }
459
+
460
+ .szl-platform-name {
461
+ font-size: 0.5625rem;
462
+ font-weight: 700;
463
+ text-transform: uppercase;
464
+ letter-spacing: 0.05em;
465
+ }
466
+
467
+ .szl-upload-zone {
468
+ display: flex;
469
+ flex-direction: column;
470
+ align-items: center;
471
+ justify-content: center;
472
+ width: 100%;
473
+ height: 10rem;
474
+ border: 2px dashed var(--border-color);
475
+ border-radius: 2.5rem;
476
+ cursor: pointer;
477
+ transition: all 0.2s ease;
478
+ overflow: hidden;
479
+ }
480
+
481
+ .szl-upload-zone:hover {
482
+ background: var(--bg-page);
483
+ border-color: rgba(99, 102, 241, 0.4);
484
+ }
485
+
486
+ .szl-upload-inner {
487
+ display: flex;
488
+ flex-direction: column;
489
+ align-items: center;
490
+ gap: 0.5rem;
491
+ }
492
+
493
+ .szl-upload-icon {
494
+ color: var(--text-muted);
495
+ opacity: 0.5;
496
+ transition: all 0.2s ease;
497
+ }
498
+
499
+ .szl-upload-zone:hover .szl-upload-icon {
500
+ opacity: 1;
501
+ color: #6366f1;
502
+ transform: scale(1.1);
503
+ }
504
+
505
+ .szl-upload-text {
506
+ font-size: 0.625rem;
507
+ font-weight: 700;
508
+ text-transform: uppercase;
509
+ color: var(--text-muted);
510
+ margin: 0;
511
+ }
512
+
513
+ .szl-file-hidden {
514
+ display: none;
515
+ }
516
+
517
+ .szl-opacity-header {
518
+ display: flex;
519
+ justify-content: space-between;
520
+ align-items: center;
521
+ }
522
+
523
+ .szl-opacity-val {
524
+ font-size: 0.75rem;
525
+ font-weight: 700;
526
+ color: #6366f1;
527
+ }
528
+
529
+ .szl-slider {
530
+ width: 100%;
531
+ height: 6px;
532
+ border-radius: 999px;
533
+ appearance: none;
534
+ background: var(--border-color);
535
+ cursor: pointer;
536
+ accent-color: #6366f1;
537
+ outline: none;
538
+ }
539
+
540
+ .szl-slider::-webkit-slider-thumb {
541
+ appearance: none;
542
+ width: 18px;
543
+ height: 18px;
544
+ background: #6366f1;
545
+ border-radius: 50%;
546
+ cursor: pointer;
547
+ border: 3px solid var(--bg-surface);
548
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.4);
549
+ }
550
+
551
+ .szl-toggles {
552
+ display: flex;
553
+ flex-direction: column;
554
+ gap: 0.75rem;
555
+ }
556
+
557
+ .szl-toggle-item {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 0.75rem;
561
+ padding: 0.75rem;
562
+ border-radius: 1rem;
563
+ background: var(--bg-page);
564
+ border: 1px solid var(--border-color);
565
+ cursor: pointer;
566
+ transition: border-color 0.2s ease;
567
+ }
568
+
569
+ .szl-toggle-item:hover {
570
+ border-color: rgba(99, 102, 241, 0.3);
571
+ }
572
+
573
+ .szl-checkbox {
574
+ width: 16px;
575
+ height: 16px;
576
+ border-radius: 4px;
577
+ accent-color: #6366f1;
578
+ cursor: pointer;
579
+ }
580
+
581
+ .szl-toggle-label {
582
+ font-size: 0.625rem;
583
+ font-weight: 700;
584
+ text-transform: uppercase;
585
+ letter-spacing: 0.05em;
586
+ color: var(--text-muted);
587
+ }
588
+
589
+ .szl-toggle-item:hover .szl-toggle-label {
590
+ color: #6366f1;
591
+ }
592
+
593
+ .szl-reset-btn {
594
+ width: 100%;
595
+ padding: 1rem;
596
+ border-radius: 2rem;
597
+ background: var(--bg-page);
598
+ border: 1px solid var(--border-color);
599
+ color: var(--text-muted);
600
+ font-size: 0.6875rem;
601
+ font-weight: 900;
602
+ text-transform: uppercase;
603
+ letter-spacing: 0.1em;
604
+ cursor: pointer;
605
+ display: flex;
606
+ align-items: center;
607
+ justify-content: center;
608
+ gap: 0.5rem;
609
+ transition: all 0.2s ease;
610
+ }
611
+
612
+ .szl-reset-btn:hover {
613
+ color: #f43f5e;
614
+ border-color: rgba(244, 63, 94, 0.3);
615
+ }
616
+
617
+ .szl-preview-area {
618
+ display: flex;
619
+ flex-direction: column;
620
+ align-items: center;
621
+ justify-content: center;
622
+ min-height: 700px;
623
+ background: var(--bg-page);
624
+ border-radius: 4rem;
625
+ padding: 2.5rem;
626
+ border: 1px solid var(--border-color);
627
+ overflow: hidden;
628
+ position: relative;
629
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
630
+ }
631
+
632
+ .szl-radial-bg {
633
+ position: absolute;
634
+ inset: 0;
635
+ background: radial-gradient(circle at center, rgba(99, 102, 241, 0.06) 0%, transparent 60%);
636
+ pointer-events: none;
637
+ }
638
+
639
+ .szl-phone {
640
+ position: relative;
641
+ background: black;
642
+ border-radius: 3.5rem;
643
+ box-shadow: 0 0 100px rgba(0, 0, 0, 0.3);
644
+ overflow: hidden;
645
+ aspect-ratio: 9 / 16;
646
+ height: 78vh;
647
+ max-height: 850px;
648
+ border: 12px solid #0f172a;
649
+ outline: 1px solid rgba(255, 255, 255, 0.05);
650
+ }
651
+
652
+ .szl-statusbar {
653
+ position: absolute;
654
+ top: 0;
655
+ left: 0;
656
+ width: 100%;
657
+ height: 2rem;
658
+ display: flex;
659
+ justify-content: space-between;
660
+ padding: 0 2rem;
661
+ align-items: center;
662
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4), transparent);
663
+ z-index: 60;
664
+ pointer-events: none;
665
+ color: white;
666
+ font-size: 0.625rem;
667
+ font-weight: 500;
668
+ box-sizing: border-box;
669
+ }
670
+
671
+ .szl-notch {
672
+ width: 6rem;
673
+ height: 1.5rem;
674
+ background: black;
675
+ border-radius: 0 0 1.5rem 1.5rem;
676
+ position: absolute;
677
+ top: 0;
678
+ left: 50%;
679
+ transform: translateX(-50%);
680
+ }
681
+
682
+ .szl-statusbar-icons {
683
+ display: flex;
684
+ gap: 6px;
685
+ align-items: center;
686
+ }
687
+
688
+ .szl-media-wrapper {
689
+ position: absolute;
690
+ inset: 0;
691
+ display: flex;
692
+ align-items: center;
693
+ justify-content: center;
694
+ overflow: hidden;
695
+ padding-top: 2.5rem;
696
+ }
697
+
698
+ .szl-empty-state {
699
+ display: flex;
700
+ flex-direction: column;
701
+ align-items: center;
702
+ color: var(--text-muted);
703
+ text-align: center;
704
+ opacity: 0.4;
705
+ transition: opacity 0.2s ease;
706
+ }
707
+
708
+ .szl-phone:hover .szl-empty-state {
709
+ opacity: 1;
710
+ }
711
+
712
+ .szl-empty-text {
713
+ font-size: 0.625rem;
714
+ font-weight: 900;
715
+ text-transform: uppercase;
716
+ letter-spacing: 0.3em;
717
+ margin: 0 0 0.5rem;
718
+ }
719
+
720
+ .szl-empty-link {
721
+ font-size: 0.5625rem;
722
+ margin: 0;
723
+ opacity: 0.5;
724
+ text-decoration: underline;
725
+ cursor: pointer;
726
+ transition: color 0.2s ease;
727
+ }
728
+
729
+ .szl-empty-link:hover {
730
+ color: #6366f1;
731
+ opacity: 1;
732
+ }
733
+
734
+ .szl-media {
735
+ position: absolute;
736
+ inset: 0;
737
+ }
738
+
739
+ .szl-hidden {
740
+ display: none;
741
+ }
742
+
743
+ .szl-overlay {
744
+ position: absolute;
745
+ inset: 0;
746
+ pointer-events: none;
747
+ z-index: 50;
748
+ transition: opacity 0.3s ease;
749
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
750
+ }
751
+
752
+ .szl-mask {
753
+ position: absolute;
754
+ inset: 0;
755
+ z-index: 40;
756
+ pointer-events: none;
757
+ display: flex;
758
+ flex-direction: column;
759
+ transition: opacity 0.5s ease;
760
+ }
761
+
762
+ .szl-mask-top {
763
+ height: 15%;
764
+ background: rgba(0, 0, 0, 0.6);
765
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
766
+ backdrop-filter: blur(4px);
767
+ }
768
+
769
+ .szl-mask-flex {
770
+ flex: 1;
771
+ }
772
+
773
+ .szl-mask-bottom {
774
+ height: 25%;
775
+ background: rgba(0, 0, 0, 0.6);
776
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
777
+ backdrop-filter: blur(4px);
778
+ }
779
+
780
+ .szl-mask-right {
781
+ position: absolute;
782
+ right: 0;
783
+ top: 20%;
784
+ bottom: 20%;
785
+ width: 18%;
786
+ background: rgba(0, 0, 0, 0.6);
787
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
788
+ backdrop-filter: blur(4px);
789
+ }
790
+
791
+ .szl-mask-hidden {
792
+ opacity: 0;
793
+ pointer-events: none;
794
+ }
795
+
796
+ .szl-grid-overlay {
797
+ position: absolute;
798
+ inset: 0;
799
+ z-index: 45;
800
+ pointer-events: none;
801
+ transition: opacity 0.3s ease;
802
+ }
803
+
804
+ .szl-grid-hidden {
805
+ opacity: 0;
806
+ }
807
+
808
+ .szl-grid-inner {
809
+ width: 100%;
810
+ height: 100%;
811
+ position: relative;
812
+ border-left: 2px solid rgba(255, 255, 255, 0.1);
813
+ border-right: 2px solid rgba(255, 255, 255, 0.1);
814
+ }
815
+
816
+ .szl-grid-h,
817
+ .szl-grid-v {
818
+ position: absolute;
819
+ background: rgba(255, 255, 255, 0.1);
820
+ }
821
+
822
+ .szl-grid-h {
823
+ left: 0;
824
+ width: 100%;
825
+ height: 2px;
826
+ }
827
+
828
+ .szl-grid-v {
829
+ top: 0;
830
+ height: 100%;
831
+ width: 2px;
832
+ }
833
+
834
+ .szl-grid-h1 { top: 33.33%; }
835
+ .szl-grid-h2 { top: 66.66%; }
836
+ .szl-grid-v1 { left: 33.33%; }
837
+ .szl-grid-v2 { left: 66.66%; }
838
+
839
+ .szl-banner {
840
+ margin-top: 2rem;
841
+ padding: 0.5rem 1.5rem;
842
+ border-radius: 999px;
843
+ background: black;
844
+ color: white;
845
+ font-size: 0.625rem;
846
+ font-weight: 900;
847
+ text-transform: uppercase;
848
+ letter-spacing: 0.2em;
849
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
850
+ outline: 1px solid rgba(255, 255, 255, 0.1);
851
+ }
852
+
853
+ @keyframes szl-spin {
854
+ from { transform: rotate(0deg); }
855
+ to { transform: rotate(360deg); }
856
+ }
857
+ </style>