@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,1514 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { TinderPhotoOptimizerUI } 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 TinderPhotoOptimizerUI;
12
+ ---
13
+
14
+ <div class="tfo-root">
15
+ <div class="tfo-container" id="tfo-optimizer">
16
+ <input type="file" id="image-input" accept=".jpg,.jpeg,.png,.webp,.gif" style="display: none;" />
17
+
18
+ <section class="tfo-editor">
19
+ <div class="tfo-simulator-wrapper" id="preview-container">
20
+ <div id="ai-loader" class="tfo-ai-loader tfo-hidden">
21
+ <div class="tfo-loader-content">
22
+ <div class="tfo-scanner-bar"></div>
23
+ <p id="ai-loader-text">{t.loaderAnalyzing}</p>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="tfo-device">
28
+ <div class="tfo-status-bar">
29
+ <span class="tfo-time">9:41</span>
30
+ <div class="tfo-dynamic-island"></div>
31
+ <div class="tfo-status-icons">
32
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
33
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 3C6.95 3 3.15 4.85 0 7.23L12 22 24 7.25C20.85 4.87 17.05 3 12 3z"/></svg>
34
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M15.67 4H14V2h-4v2H8.33C7.6 4 7 4.6 7 5.33v15.33C7 21.4 7.6 22 8.33 22h7.33c.74 0 1.34-.6 1.34-1.33V5.33C17 4.6 16.4 4 15.67 4z"/></svg>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="tfo-app-header">
39
+ <div class="tfo-icon-placeholder"></div>
40
+ <div class="tfo-logo">
41
+ <svg width="30" height="30" viewBox="0 0 24 24">
42
+ <defs>
43
+ <linearGradient id="tfoFlameGrad" x1="0%" y1="0%" x2="0%" y2="100%">
44
+ <stop offset="0%" stop-color="#FD297B" />
45
+ <stop offset="100%" stop-color="#FF5864" />
46
+ </linearGradient>
47
+ </defs>
48
+ <path fill="url(#tfoFlameGrad)" d="M12.51 21s4.89-2 4.89-7.14c0-3.32-2.55-5.26-4.52-6.57-1.42.34-3.53 1.55-3.53 4.28 0 2.27 1.89 3.1 1.89 4.84 0 2.11-2.14 3.03-3.6 3.03-1.63 0-3.14-.98-3.14-3.32 0-3.8 2.55-6.68 5.4-10.12C11.51 2 12.51 2 12.51 2c-.67 2.01 1.39 3.63 2.5 5.23C16.89 9.87 19.33 11.52 19.33 15c0 5-5.83 6-6.82 6z"/>
49
+ </svg>
50
+ </div>
51
+ <div class="tfo-settings-icon">
52
+ <button id="reset-tool" class="tfo-reset-btn" title="Reset" style="display: none;">
53
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
54
+ </button>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="tfo-card-container">
59
+ <div class="tfo-main-card" id="drop-zone">
60
+ <canvas id="editor-canvas" class="tfo-canvas"></canvas>
61
+
62
+ <div class="tfo-upload-overlay" id="upload-overlay">
63
+ <div class="tfo-upload-circle">
64
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
65
+ </div>
66
+ <h3>{t.uploadTitle}</h3>
67
+ <p>{t.uploadSubtitle}</p>
68
+ </div>
69
+
70
+ <div class="tfo-card-overlay">
71
+ <div class="tfo-progress">
72
+ <div class="tfo-segment tfo-segment--active"></div>
73
+ <div class="tfo-segment"></div>
74
+ <div class="tfo-segment"></div>
75
+ </div>
76
+ <div class="tfo-card-info">
77
+ <div class="tfo-primary-info">
78
+ <h2 class="tfo-name">{t.simulatorName}</h2>
79
+ <div class="tfo-badge">
80
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="#fff" stroke="#fff" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
81
+ </div>
82
+ </div>
83
+ <div class="tfo-secondary-info">
84
+ <span class="tfo-job">{t.simulatorJob}</span>
85
+ <div class="tfo-tags">
86
+ <span class="tfo-tag">
87
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
88
+ {t.simulatorCity}
89
+ </span>
90
+ <span class="tfo-tag">
91
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
92
+ {t.simulatorDistance}
93
+ </span>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="tfo-grid-overlay" id="grid-overlay" style="display: grid;">
100
+ <div></div><div></div><div></div>
101
+ <div></div><div></div><div></div>
102
+ <div></div><div></div><div></div>
103
+ </div>
104
+
105
+ <div class="tfo-deadzones" id="deadzones-overlay" style="display: block;">
106
+ <div class="tfo-deadzone tfo-deadzone--top"><span>{t.deadzoneTop}</span></div>
107
+ <div class="tfo-deadzone tfo-deadzone--bottom"><span>{t.deadzoneBottom}</span></div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="tfo-device-actions">
113
+ <button class="tfo-btn-circular tfo-btn-rewind" title="Rewind">
114
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f6be00" stroke-width="2.5"><path d="M2.5 2v6h6M2.66 15.57a10 10 0 1 0 .57-8.38"/></svg>
115
+ </button>
116
+ <button class="tfo-btn-circular tfo-btn-nope" title="Nope">
117
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#f63a6e" stroke-width="3"><path d="M18 6L6 18M6 6l12 12"/></svg>
118
+ </button>
119
+ <button class="tfo-btn-circular tfo-btn-superlike" title="Super Like">
120
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="#3CA4FF"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/></svg>
121
+ </button>
122
+ <button class="tfo-btn-circular tfo-btn-like" title="Like">
123
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="#1BE4A1"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
124
+ </button>
125
+ <button class="tfo-btn-circular tfo-btn-boost" title="Boost">
126
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#a65cf0"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
127
+ </button>
128
+ </div>
129
+
130
+ <div class="tfo-nav-bar">
131
+ <div class="tfo-nav-item tfo-nav-item--active">
132
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M12.51 21s4.89-2 4.89-7.14c0-3.32-2.55-5.26-4.52-6.57-1.42.34-3.53 1.55-3.53 4.28 0 2.27 1.89 3.1 1.89 4.84 0 2.11-2.14 3.03-3.6 3.03-1.63 0-3.14-.98-3.14-3.32 0-3.8 2.55-6.68 5.4-10.12C11.51 2 12.51 2 12.51 2c-.67 2.01 1.39 3.63 2.5 5.23C16.89 9.87 19.33 11.52 19.33 15c0 5-5.83 6-6.82 6z"/></svg>
133
+ </div>
134
+ <div class="tfo-nav-item"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg></div>
135
+ <div class="tfo-nav-item"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></div>
136
+ <div class="tfo-nav-item"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>
137
+ <div class="tfo-nav-item"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="tfo-analysis-board">
143
+ <div class="tfo-analysis-col tfo-analysis-col--success" id="group-success">
144
+ <div class="tfo-col-header">
145
+ <div class="tfo-col-icon">
146
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
147
+ </div>
148
+ <span>{t.analysisSuccess}</span>
149
+ </div>
150
+ <div class="tfo-alerts-stack" id="stack-success"></div>
151
+ </div>
152
+ <div class="tfo-analysis-col tfo-analysis-col--warning" id="group-warning">
153
+ <div class="tfo-col-header">
154
+ <div class="tfo-col-icon">
155
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
156
+ </div>
157
+ <span>{t.analysisWarning}</span>
158
+ </div>
159
+ <div class="tfo-alerts-stack" id="stack-warning"></div>
160
+ </div>
161
+ <div class="tfo-analysis-col tfo-analysis-col--error" id="group-error">
162
+ <div class="tfo-col-header">
163
+ <div class="tfo-col-icon">
164
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
165
+ </div>
166
+ <span>{t.analysisError}</span>
167
+ </div>
168
+ <div class="tfo-alerts-stack" id="stack-error"></div>
169
+ </div>
170
+ </div>
171
+ </section>
172
+
173
+ <aside class="tfo-sidebar">
174
+ <div class="tfo-panel">
175
+ <h3>{t.panelManual}</h3>
176
+ <div class="tfo-control-group">
177
+ <label>{t.labelZoom} <span>1.0x</span></label>
178
+ <input type="range" id="zoom-slider" min="1" max="3" step="0.1" value="1" />
179
+ </div>
180
+ <div class="tfo-control-group">
181
+ <label>{t.labelBrightness} <span>100%</span></label>
182
+ <input type="range" id="brightness-slider" min="50" max="150" step="1" value="100" />
183
+ </div>
184
+ </div>
185
+
186
+ <div class="tfo-panel">
187
+ <h3>{t.panelVisualization}</h3>
188
+ <div class="tfo-toggles-grid">
189
+ <label class="tfo-switch-item">
190
+ <div class="tfo-switch-info">
191
+ <span class="tfo-switch-label">{t.toggleGrid}</span>
192
+ <span class="tfo-switch-desc">{t.toggleGridDesc}</span>
193
+ </div>
194
+ <div class="tfo-switch-wrapper">
195
+ <input type="checkbox" id="toggle-grid-switch" checked />
196
+ <span class="tfo-track"></span>
197
+ </div>
198
+ </label>
199
+ <label class="tfo-switch-item">
200
+ <div class="tfo-switch-info">
201
+ <span class="tfo-switch-label">{t.toggleDeadzones}</span>
202
+ <span class="tfo-switch-desc">{t.toggleDeadzonesDesc}</span>
203
+ </div>
204
+ <div class="tfo-switch-wrapper">
205
+ <input type="checkbox" id="toggle-deadzones-switch" checked />
206
+ <span class="tfo-track"></span>
207
+ </div>
208
+ </label>
209
+ <label class="tfo-switch-item" id="face-box-container" style="opacity: 0.5; pointer-events: none;">
210
+ <div class="tfo-switch-info">
211
+ <span class="tfo-switch-label">{t.toggleFaceBox}</span>
212
+ <span class="tfo-switch-desc">{t.toggleFaceBoxDesc}</span>
213
+ </div>
214
+ <div class="tfo-switch-wrapper">
215
+ <input type="checkbox" id="toggle-face-box-switch" checked />
216
+ <span class="tfo-track"></span>
217
+ </div>
218
+ </label>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="tfo-panel">
223
+ <h3>{t.panelMetrics}</h3>
224
+ <div class="tfo-metrics-grid">
225
+ <div class="tfo-metric-card">
226
+ <span class="tfo-metric-value" id="metric-face-size">0%</span>
227
+ <span class="tfo-metric-label">{t.metricFaceSize}</span>
228
+ </div>
229
+ <div class="tfo-metric-card">
230
+ <span class="tfo-metric-value" id="metric-eye-line">--</span>
231
+ <span class="tfo-metric-label">{t.metricEyeLine}</span>
232
+ </div>
233
+ <div class="tfo-metric-card">
234
+ <span class="tfo-metric-value" id="metric-coverage">--</span>
235
+ <span class="tfo-metric-label">{t.metricCoverage}</span>
236
+ </div>
237
+ <div class="tfo-metric-card">
238
+ <span class="tfo-metric-value" id="metric-mood">--</span>
239
+ <span class="tfo-metric-label">{t.metricMood}</span>
240
+ </div>
241
+ <div class="tfo-metric-card">
242
+ <span class="tfo-metric-value" id="metric-quality">--</span>
243
+ <span class="tfo-metric-label">{t.metricQuality}</span>
244
+ </div>
245
+ </div>
246
+ </div>
247
+
248
+ <div class="tfo-panel tfo-actions-panel">
249
+ <div class="tfo-action-grid">
250
+ <button class="tfo-action-btn" id="relaunch-ai">
251
+ <div class="tfo-btn-icon">
252
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
253
+ </div>
254
+ <span>{t.btnRescan}</span>
255
+ </button>
256
+ <button class="tfo-action-btn" id="auto-focus" style="display: none;">
257
+ <div class="tfo-btn-icon">
258
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
259
+ </div>
260
+ <span>{t.btnAutoFocus}</span>
261
+ </button>
262
+ <button class="tfo-action-btn" id="manual-face">
263
+ <div class="tfo-btn-icon">
264
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6"/><path d="M16 11h6"/></svg>
265
+ </div>
266
+ <span>{t.btnMarkFace}</span>
267
+ </button>
268
+ </div>
269
+ </div>
270
+
271
+ <div class="tfo-panel">
272
+ <h3>{t.panelExport}</h3>
273
+ <p class="tfo-export-desc">{t.panelExportDesc}</p>
274
+ <button class="tfo-export-btn" id="export-button">{t.btnDownload}</button>
275
+ </div>
276
+ </aside>
277
+ </div>
278
+ </div>
279
+
280
+ <script is:inline src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
281
+
282
+ <script>
283
+ import { TinderOptimizerEngine } from './logic';
284
+
285
+ class TinderOptimizerUI {
286
+ private engine: TinderOptimizerEngine;
287
+ private canvas: HTMLCanvasElement;
288
+ private input: HTMLInputElement;
289
+ private dropZone: HTMLElement;
290
+ private zoomSlider: HTMLInputElement;
291
+ private brightnessSlider: HTMLInputElement;
292
+ private exportBtn: HTMLButtonElement;
293
+ private toggleGridSwitch: HTMLInputElement;
294
+ private toggleDeadzonesSwitch: HTMLInputElement;
295
+ private toggleFaceBoxSwitch: HTMLInputElement;
296
+ private faceBoxContainer: HTMLElement;
297
+ private relaunchAiBtn: HTMLButtonElement;
298
+ private autofocusBtn: HTMLButtonElement;
299
+ private manualFaceBtn: HTMLButtonElement;
300
+ private aiLoader: HTMLElement;
301
+ private aiLoaderText: HTMLElement;
302
+ private resetBtn: HTMLButtonElement;
303
+ private uploadOverlay: HTMLElement;
304
+
305
+ private isDragging = false;
306
+ private isSelecting = false;
307
+ private selectionRect = { x: 0, y: 0, w: 0, h: 0 };
308
+ private lastPos = { x: 0, y: 0 };
309
+
310
+ constructor() {
311
+ this.canvas = document.getElementById('editor-canvas') as HTMLCanvasElement;
312
+ this.engine = new TinderOptimizerEngine(this.canvas);
313
+ this.input = document.getElementById('image-input') as HTMLInputElement;
314
+ this.dropZone = document.getElementById('drop-zone') as HTMLElement;
315
+ this.zoomSlider = document.getElementById('zoom-slider') as HTMLInputElement;
316
+ this.brightnessSlider = document.getElementById('brightness-slider') as HTMLInputElement;
317
+ this.exportBtn = document.getElementById('export-button') as HTMLButtonElement;
318
+ this.toggleGridSwitch = document.getElementById('toggle-grid-switch') as HTMLInputElement;
319
+ this.toggleDeadzonesSwitch = document.getElementById('toggle-deadzones-switch') as HTMLInputElement;
320
+ this.toggleFaceBoxSwitch = document.getElementById('toggle-face-box-switch') as HTMLInputElement;
321
+ this.faceBoxContainer = document.getElementById('face-box-container') as HTMLElement;
322
+ this.relaunchAiBtn = document.getElementById('relaunch-ai') as HTMLButtonElement;
323
+ this.autofocusBtn = document.getElementById('auto-focus') as HTMLButtonElement;
324
+ this.manualFaceBtn = document.getElementById('manual-face') as HTMLButtonElement;
325
+ this.aiLoader = document.getElementById('ai-loader') as HTMLElement;
326
+ this.aiLoaderText = document.getElementById('ai-loader-text') as HTMLElement;
327
+ this.resetBtn = document.getElementById('reset-tool') as HTMLButtonElement;
328
+ this.uploadOverlay = document.getElementById('upload-overlay') as HTMLElement;
329
+ this.init();
330
+ }
331
+
332
+ private setupDropZone() {
333
+ this.dropZone.addEventListener('click', () => {
334
+ if (!this.engine.image) this.input.click();
335
+ });
336
+ this.input.addEventListener('change', (e) => this.handleImage(e));
337
+ this.dropZone.addEventListener('dragover', (e) => { e.preventDefault(); });
338
+ this.dropZone.addEventListener('drop', (e) => { e.preventDefault(); this.loadFile((e as DragEvent).dataTransfer?.files[0]); });
339
+ }
340
+
341
+ private setupSliders() {
342
+ this.zoomSlider.addEventListener('input', () => {
343
+ const val = parseFloat(this.zoomSlider.value);
344
+ this.updateState();
345
+ const span = this.zoomSlider.parentElement?.querySelector('span');
346
+ if (span) span.textContent = `${val.toFixed(1)}x`;
347
+ });
348
+ this.brightnessSlider.addEventListener('input', () => {
349
+ const val = parseInt(this.brightnessSlider.value);
350
+ this.updateState();
351
+ const span = this.brightnessSlider.parentElement?.querySelector('span');
352
+ if (span) span.textContent = `${val}%`;
353
+ });
354
+ }
355
+
356
+ private setupToggles() {
357
+ this.toggleGridSwitch.addEventListener('change', () => {
358
+ const grid = document.getElementById('grid-overlay');
359
+ if (grid) grid.style.display = this.toggleGridSwitch.checked ? 'grid' : 'none';
360
+ });
361
+ this.toggleDeadzonesSwitch.addEventListener('change', () => {
362
+ const dead = document.getElementById('deadzones-overlay');
363
+ if (dead) dead.style.display = this.toggleDeadzonesSwitch.checked ? 'block' : 'none';
364
+ });
365
+ this.toggleFaceBoxSwitch.addEventListener('change', () => this.draw());
366
+ }
367
+
368
+ private setupButtons() {
369
+ this.relaunchAiBtn.addEventListener('click', () => this.runDetection());
370
+ this.autofocusBtn.addEventListener('click', () => this.applyAutoFocus());
371
+ this.manualFaceBtn.addEventListener('click', () => {
372
+ this.isSelecting = !this.isSelecting;
373
+ this.manualFaceBtn.classList.toggle('is-active', this.isSelecting);
374
+ });
375
+ this.canvas.addEventListener('mousedown', (e) => this.onStart(e));
376
+ window.addEventListener('mousemove', (e) => this.onMove(e));
377
+ window.addEventListener('mouseup', () => this.onEnd());
378
+ this.canvas.addEventListener('touchstart', (e) => this.onStart(e), { passive: false });
379
+ window.addEventListener('touchmove', (e) => this.onMove(e), { passive: false });
380
+ window.addEventListener('touchend', () => this.onEnd());
381
+ this.exportBtn.addEventListener('click', () => {
382
+ this.draw(false);
383
+ const link = document.createElement('a');
384
+ link.download = `tinder-opt-${Date.now()}.jpg`;
385
+ link.href = this.canvas.toDataURL('image/jpeg', 0.9);
386
+ link.click();
387
+ this.draw();
388
+ });
389
+ this.resetBtn.addEventListener('click', () => this.reset());
390
+ }
391
+
392
+ private init() {
393
+ this.setupDropZone();
394
+ this.setupSliders();
395
+ this.setupToggles();
396
+ this.setupButtons();
397
+ }
398
+
399
+ private reset() {
400
+ this.engine.image = null;
401
+ this.engine.faceDetection = null;
402
+ this.uploadOverlay.classList.remove('tfo-hidden');
403
+ this.resetBtn.style.display = 'none';
404
+ this.autofocusBtn.style.display = 'none';
405
+ this.faceBoxContainer.style.opacity = '0.5';
406
+ this.faceBoxContainer.style.pointerEvents = 'none';
407
+ const ctx = this.canvas.getContext('2d');
408
+ if (ctx) ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
409
+ this.updateAnalysis();
410
+ }
411
+
412
+ private handleImage(e: Event) {
413
+ const input = e.target as HTMLInputElement;
414
+ if (input.files?.[0]) this.loadFile(input.files[0]);
415
+ }
416
+
417
+ private async loadFile(file?: File) {
418
+ if (!file) return;
419
+ const reader = new FileReader();
420
+ reader.onload = (e) => {
421
+ const img = new Image();
422
+ img.onload = async () => {
423
+ this.canvas.width = 1080;
424
+ this.canvas.height = 1920;
425
+ await this.runDetection(img);
426
+ this.draw();
427
+ this.uploadOverlay.classList.add('tfo-hidden');
428
+ this.resetBtn.style.display = 'flex';
429
+ };
430
+ img.src = e.target?.result as string;
431
+ };
432
+ reader.readAsDataURL(file);
433
+ }
434
+
435
+ private async runDetection(img?: HTMLImageElement) {
436
+ const targetImg = img || this.engine.image;
437
+ if (!targetImg) return;
438
+ this.aiLoader.classList.remove('tfo-hidden');
439
+ this.aiLoaderText.textContent = 'Cargando modelos de visión...';
440
+ await this.engine.loadModels();
441
+ this.aiLoaderText.textContent = 'Analizando composición...';
442
+ const face = await this.engine.detect(targetImg);
443
+ if (face) {
444
+ this.autofocusBtn.style.display = 'flex';
445
+ this.faceBoxContainer.style.opacity = '1';
446
+ this.faceBoxContainer.style.pointerEvents = 'auto';
447
+ }
448
+ this.aiLoader.classList.add('tfo-hidden');
449
+ this.updateState();
450
+ }
451
+
452
+ private onStart(e: MouseEvent | TouchEvent) {
453
+ if (e instanceof TouchEvent) e.preventDefault();
454
+ const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
455
+ const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
456
+ const rect = this.canvas.getBoundingClientRect();
457
+ const x = (clientX - rect.left) * (this.canvas.width / rect.width);
458
+ const y = (clientY - rect.top) * (this.canvas.height / rect.height);
459
+ if (this.isSelecting) {
460
+ this.selectionRect = { x, y, w: 0, h: 0 };
461
+ } else {
462
+ this.isDragging = true;
463
+ this.lastPos = { x: clientX, y: clientY };
464
+ }
465
+ }
466
+
467
+ private getClientPos(e: MouseEvent | TouchEvent) {
468
+ const src = e instanceof MouseEvent ? e : e.touches[0];
469
+ return { x: src.clientX, y: src.clientY };
470
+ }
471
+
472
+ private handleSelectionMove(clientX: number, clientY: number) {
473
+ const rect = this.canvas.getBoundingClientRect();
474
+ this.selectionRect.w = (clientX - rect.left) * (this.canvas.width / rect.width) - this.selectionRect.x;
475
+ this.selectionRect.h = (clientY - rect.top) * (this.canvas.height / rect.height) - this.selectionRect.y;
476
+ this.draw();
477
+ }
478
+
479
+ private handleDragMove(clientX: number, clientY: number) {
480
+ const dx = (clientX - this.lastPos.x) * (this.canvas.width / this.canvas.clientWidth);
481
+ const dy = (clientY - this.lastPos.y) * (this.canvas.height / this.canvas.clientHeight);
482
+ const offset = this.engine.offset;
483
+ this.engine.updateState(parseFloat(this.zoomSlider.value), { x: offset.x + dx, y: offset.y + dy }, parseInt(this.brightnessSlider.value));
484
+ this.lastPos = { x: clientX, y: clientY };
485
+ this.draw();
486
+ this.updateAnalysis();
487
+ }
488
+
489
+ private isActivePress(e: MouseEvent | TouchEvent) {
490
+ return e instanceof MouseEvent ? e.buttons === 1 : e.touches.length > 0;
491
+ }
492
+
493
+ private onMove(e: MouseEvent | TouchEvent) {
494
+ if (this.isDragging || this.isSelecting) {
495
+ if (e instanceof TouchEvent) e.preventDefault();
496
+ }
497
+ const { x: clientX, y: clientY } = this.getClientPos(e);
498
+ if (this.isSelecting && (this.selectionRect.w !== 0 || this.isActivePress(e))) {
499
+ this.handleSelectionMove(clientX, clientY);
500
+ } else if (this.isDragging) {
501
+ this.handleDragMove(clientX, clientY);
502
+ }
503
+ }
504
+
505
+ private onEnd() {
506
+ if (this.isSelecting && Math.abs(this.selectionRect.w) > 20) {
507
+ this.engine.setManualFace(this.selectionRect);
508
+ this.isSelecting = false;
509
+ this.manualFaceBtn.classList.remove('is-active');
510
+ this.autofocusBtn.style.display = 'flex';
511
+ this.faceBoxContainer.style.opacity = '1';
512
+ this.faceBoxContainer.style.pointerEvents = 'auto';
513
+ this.updateState();
514
+ }
515
+ this.isDragging = false;
516
+ this.selectionRect = { x: 0, y: 0, w: 0, h: 0 };
517
+ this.draw();
518
+ }
519
+
520
+ private updateState() {
521
+ this.engine.updateState(parseFloat(this.zoomSlider.value), this.engine.offset, parseInt(this.brightnessSlider.value));
522
+ this.draw();
523
+ this.updateAnalysis();
524
+ }
525
+
526
+ private applyAutoFocus() {
527
+ const transform = this.engine.getOptimalTransform();
528
+ if (transform) {
529
+ this.zoomSlider.value = transform.zoom.toString();
530
+ const span = this.zoomSlider.parentElement?.querySelector('span');
531
+ if (span) span.textContent = `${transform.zoom.toFixed(1)}x`;
532
+ this.engine.updateState(transform.zoom, { x: transform.x, y: transform.y }, parseInt(this.brightnessSlider.value));
533
+ this.draw();
534
+ this.updateAnalysis();
535
+ }
536
+ }
537
+
538
+ private updateMetrics() {
539
+ const { metrics } = this.engine.getAnalysis();
540
+ const mFace = document.getElementById('metric-face-size');
541
+ const mEye = document.getElementById('metric-eye-line');
542
+ const mCoverage = document.getElementById('metric-coverage');
543
+ const mMood = document.getElementById('metric-mood');
544
+ const mQuality = document.getElementById('metric-quality');
545
+ if (mFace) mFace.textContent = metrics.faceSize;
546
+ if (mEye) mEye.textContent = metrics.eyeLine;
547
+ if (mCoverage) mCoverage.textContent = metrics.coverage;
548
+ if (mMood) mMood.textContent = metrics.mood;
549
+ if (mQuality) mQuality.textContent = metrics.quality;
550
+ }
551
+
552
+ private renderAlerts(alerts: { type: string; text: string }[], stacks: Record<string, HTMLElement | null>) {
553
+ Object.values(stacks).forEach((s) => { if (s) s.innerHTML = ''; });
554
+ if (alerts.length === 0) {
555
+ if (stacks.success) stacks.success.innerHTML = '<div class="tfo-alert tfo-alert--success">Composición técnica excelente.</div>';
556
+ } else {
557
+ alerts.forEach((a) => {
558
+ const stack = stacks[a.type];
559
+ if (stack) {
560
+ const div = document.createElement('div');
561
+ div.className = `tfo-alert tfo-alert--${a.type}`;
562
+ div.textContent = a.text;
563
+ stack.appendChild(div);
564
+ }
565
+ });
566
+ }
567
+ ['success', 'warning', 'error'].forEach((type) => {
568
+ const group = document.getElementById(`group-${type}`);
569
+ const stack = stacks[type];
570
+ if (group && stack) group.style.display = stack.children.length > 0 ? 'flex' : 'none';
571
+ });
572
+ }
573
+
574
+ private updateAnalysis() {
575
+ const { alerts } = this.engine.getAnalysis();
576
+ this.updateMetrics();
577
+ const stacks: Record<string, HTMLElement | null> = {
578
+ success: document.getElementById('stack-success'),
579
+ warning: document.getElementById('stack-warning'),
580
+ error: document.getElementById('stack-error'),
581
+ };
582
+ this.renderAlerts(alerts, stacks);
583
+ }
584
+
585
+ draw(forceShowFaceBox?: boolean) {
586
+ const showFaceBox = forceShowFaceBox !== undefined ? forceShowFaceBox : this.toggleFaceBoxSwitch.checked;
587
+ this.engine.draw(showFaceBox);
588
+ if (this.isSelecting && this.selectionRect.w !== 0) {
589
+ const ctx = this.canvas.getContext('2d')!;
590
+ ctx.strokeStyle = '#FD297B';
591
+ ctx.setLineDash([10, 5]);
592
+ ctx.strokeRect(this.selectionRect.x, this.selectionRect.y, this.selectionRect.w, this.selectionRect.h);
593
+ ctx.setLineDash([]);
594
+ }
595
+ }
596
+ }
597
+
598
+ let uiInstance: TinderOptimizerUI | null = null;
599
+
600
+ function init() {
601
+ if (uiInstance) return;
602
+ const container = document.getElementById('tfo-optimizer');
603
+ if (!container) return;
604
+ uiInstance = new TinderOptimizerUI();
605
+ }
606
+
607
+ document.addEventListener('astro:page-load', () => {
608
+ uiInstance = null;
609
+ init();
610
+ });
611
+
612
+ init();
613
+ </script>
614
+
615
+ <style>
616
+ .tfo-root {
617
+ --tfo-pink: #FD297B;
618
+ --tfo-orange: #FF5864;
619
+ --tfo-gradient: linear-gradient(45deg, #FD297B, #FF5864, #FF655B);
620
+ --tfo-text: #1a1a1a;
621
+ --tfo-muted: #64748b;
622
+ --tfo-bg: #f8fafc;
623
+ --tfo-panel-bg: rgba(255, 255, 255, 0.95);
624
+ --tfo-panel-border: rgba(255, 255, 255, 0.3);
625
+ --tfo-metric-bg: #f8fafc;
626
+ --tfo-metric-border: #f1f5f9;
627
+ --tfo-action-bg: #fff;
628
+ --tfo-action-border: #f1f5f9;
629
+ --tfo-action-color: #475569;
630
+ --tfo-btn-icon-bg: #f8fafc;
631
+ --tfo-btn-icon-color: #64748b;
632
+ --tfo-analysis-bg: #fff;
633
+ --tfo-alert-color: #1a1a1a;
634
+ --tfo-switch-off: #e2e8f0;
635
+ }
636
+
637
+ :global(.theme-dark) .tfo-root {
638
+ --tfo-text: #f1f5f9;
639
+ --tfo-muted: #94a3b8;
640
+ --tfo-bg: #0f172a;
641
+ --tfo-panel-bg: rgba(15, 23, 42, 0.85);
642
+ --tfo-panel-border: rgba(255, 255, 255, 0.08);
643
+ --tfo-metric-bg: rgba(255, 255, 255, 0.05);
644
+ --tfo-metric-border: rgba(255, 255, 255, 0.08);
645
+ --tfo-action-bg: rgba(255, 255, 255, 0.04);
646
+ --tfo-action-border: rgba(255, 255, 255, 0.08);
647
+ --tfo-action-color: #94a3b8;
648
+ --tfo-btn-icon-bg: rgba(255, 255, 255, 0.06);
649
+ --tfo-btn-icon-color: #94a3b8;
650
+ --tfo-analysis-bg: rgba(15, 23, 42, 0.6);
651
+ --tfo-alert-color: #e2e8f0;
652
+ --tfo-switch-off: #334155;
653
+ }
654
+
655
+ .tfo-container {
656
+ display: grid;
657
+ grid-template-columns: 1fr 380px;
658
+ gap: 3rem;
659
+ padding: 3rem;
660
+ max-width: 1300px;
661
+ margin: 2rem auto;
662
+ color: var(--tfo-text);
663
+ background: var(--tfo-bg);
664
+ border-radius: 3rem;
665
+ border: 1px solid rgba(0, 0, 0, 0.05);
666
+ box-shadow: 0 40px 80px rgba(0, 0, 0, 0.08);
667
+ position: relative;
668
+ overflow: hidden;
669
+ }
670
+
671
+ :global(.theme-dark) .tfo-container {
672
+ border-color: rgba(255, 255, 255, 0.06);
673
+ }
674
+
675
+ .tfo-container::before {
676
+ content: '';
677
+ position: absolute;
678
+ top: -20%;
679
+ right: -10%;
680
+ width: 60%;
681
+ height: 60%;
682
+ background: radial-gradient(circle at center, rgba(254, 60, 114, 0.08) 0%, transparent 70%);
683
+ pointer-events: none;
684
+ }
685
+
686
+ @media (max-width: 1200px) {
687
+ .tfo-container {
688
+ grid-template-columns: 1fr;
689
+ gap: 2rem;
690
+ padding: 2rem;
691
+ margin: 1rem;
692
+ border-radius: 2rem;
693
+ }
694
+ .tfo-sidebar { order: 2; }
695
+ .tfo-editor { order: 1; }
696
+ }
697
+
698
+ @media (max-width: 640px) {
699
+ .tfo-container {
700
+ padding: 1.25rem;
701
+ margin: 0.5rem;
702
+ gap: 1.5rem;
703
+ border-radius: 1.5rem;
704
+ }
705
+ .tfo-panel {
706
+ padding: 1.25rem;
707
+ border-radius: 20px;
708
+ }
709
+ .tfo-analysis-board { margin-top: 1.5rem; }
710
+ .tfo-alert { font-size: 0.9rem; }
711
+ }
712
+
713
+ .tfo-editor {
714
+ display: flex;
715
+ flex-direction: column;
716
+ gap: 2rem;
717
+ min-width: 0;
718
+ }
719
+
720
+ .tfo-simulator-wrapper {
721
+ position: relative;
722
+ width: 100%;
723
+ max-width: 380px;
724
+ margin: 0 auto;
725
+ }
726
+
727
+ .tfo-sidebar {
728
+ display: flex;
729
+ flex-direction: column;
730
+ gap: 1.5rem;
731
+ }
732
+
733
+ .tfo-panel {
734
+ background: var(--tfo-panel-bg);
735
+ backdrop-filter: blur(20px);
736
+ border: 1px solid var(--tfo-panel-border);
737
+ border-radius: 28px;
738
+ padding: 1.8rem;
739
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03), 0 1px 1px rgba(0, 0, 0, 0.01);
740
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
741
+ }
742
+
743
+ .tfo-panel:hover {
744
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.07);
745
+ transform: translateY(-4px);
746
+ border-color: rgba(253, 41, 123, 0.1);
747
+ }
748
+
749
+ .tfo-panel h3 {
750
+ font-size: 0.75rem;
751
+ margin: 0 0 1.2rem;
752
+ color: var(--tfo-pink);
753
+ text-transform: uppercase;
754
+ letter-spacing: 1.2px;
755
+ font-weight: 800;
756
+ }
757
+
758
+ .tfo-control-group {
759
+ display: flex;
760
+ flex-direction: column;
761
+ gap: 0.8rem;
762
+ margin-bottom: 1.2rem;
763
+ }
764
+
765
+ .tfo-control-group label {
766
+ font-size: 0.85rem;
767
+ font-weight: 600;
768
+ color: var(--tfo-muted);
769
+ display: flex;
770
+ justify-content: space-between;
771
+ }
772
+
773
+ input[type="range"] {
774
+ width: 100%;
775
+ accent-color: var(--tfo-pink);
776
+ }
777
+
778
+ .tfo-toggles-grid {
779
+ display: flex;
780
+ flex-direction: column;
781
+ gap: 1.2rem;
782
+ }
783
+
784
+ .tfo-switch-item {
785
+ display: flex;
786
+ align-items: center;
787
+ justify-content: space-between;
788
+ cursor: pointer;
789
+ padding: 0.5rem 0;
790
+ }
791
+
792
+ .tfo-switch-info {
793
+ display: flex;
794
+ flex-direction: column;
795
+ gap: 0.2rem;
796
+ }
797
+
798
+ .tfo-switch-label {
799
+ font-size: 0.95rem;
800
+ font-weight: 700;
801
+ color: var(--tfo-text);
802
+ transition: color 0.2s;
803
+ }
804
+
805
+ .tfo-switch-item:hover .tfo-switch-label { color: var(--tfo-pink); }
806
+
807
+ .tfo-switch-desc {
808
+ font-size: 0.75rem;
809
+ color: var(--tfo-muted);
810
+ }
811
+
812
+ .tfo-switch-wrapper {
813
+ position: relative;
814
+ display: inline-block;
815
+ width: 44px;
816
+ height: 24px;
817
+ flex-shrink: 0;
818
+ }
819
+
820
+ .tfo-switch-wrapper input {
821
+ opacity: 0;
822
+ width: 0;
823
+ height: 0;
824
+ }
825
+
826
+ .tfo-track {
827
+ position: absolute;
828
+ cursor: pointer;
829
+ inset: 0;
830
+ background-color: var(--tfo-switch-off);
831
+ transition: 0.4s;
832
+ border-radius: 24px;
833
+ }
834
+
835
+ .tfo-track::before {
836
+ position: absolute;
837
+ content: "";
838
+ height: 18px;
839
+ width: 18px;
840
+ left: 3px;
841
+ bottom: 3px;
842
+ background-color: white;
843
+ transition: 0.4s;
844
+ border-radius: 50%;
845
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
846
+ }
847
+
848
+ input:checked + .tfo-track { background: var(--tfo-gradient); }
849
+ input:checked + .tfo-track::before { transform: translateX(20px); }
850
+
851
+ .tfo-metrics-grid {
852
+ display: grid;
853
+ grid-template-columns: repeat(3, 1fr);
854
+ gap: 0.8rem;
855
+ margin-top: 0.5rem;
856
+ }
857
+
858
+ @media (max-width: 480px) {
859
+ .tfo-metrics-grid { grid-template-columns: repeat(2, 1fr); }
860
+ }
861
+
862
+ .tfo-metric-card {
863
+ background: var(--tfo-metric-bg);
864
+ padding: 0.8rem 0.4rem;
865
+ border-radius: 16px;
866
+ display: flex;
867
+ flex-direction: column;
868
+ align-items: center;
869
+ text-align: center;
870
+ border: 1px solid var(--tfo-metric-border);
871
+ transition: all 0.3s ease;
872
+ min-width: 0;
873
+ overflow: hidden;
874
+ }
875
+
876
+ .tfo-metric-card:hover {
877
+ border-color: var(--tfo-pink);
878
+ box-shadow: 0 4px 12px rgba(253, 41, 123, 0.05);
879
+ transform: scale(1.05);
880
+ }
881
+
882
+ .tfo-metric-value {
883
+ font-size: 0.85rem;
884
+ font-weight: 800;
885
+ color: var(--tfo-pink);
886
+ margin-bottom: 2px;
887
+ white-space: nowrap;
888
+ overflow: hidden;
889
+ text-overflow: ellipsis;
890
+ max-width: 100%;
891
+ }
892
+
893
+ .tfo-metric-label {
894
+ font-size: 0.65rem;
895
+ text-transform: uppercase;
896
+ letter-spacing: 0.5px;
897
+ color: var(--tfo-muted);
898
+ font-weight: 600;
899
+ }
900
+
901
+ .tfo-actions-panel { padding: 1.2rem; }
902
+
903
+ .tfo-action-grid {
904
+ display: grid;
905
+ grid-template-columns: 1fr 1fr;
906
+ gap: 1rem;
907
+ }
908
+
909
+ .tfo-action-btn {
910
+ display: flex;
911
+ flex-direction: column;
912
+ align-items: center;
913
+ gap: 0.6rem;
914
+ padding: 1rem 0.5rem;
915
+ background: var(--tfo-action-bg);
916
+ border: 1.5px solid var(--tfo-action-border);
917
+ border-radius: 16px;
918
+ color: var(--tfo-action-color);
919
+ cursor: pointer;
920
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
921
+ }
922
+
923
+ .tfo-btn-icon {
924
+ width: 40px;
925
+ height: 40px;
926
+ background: var(--tfo-btn-icon-bg);
927
+ border-radius: 12px;
928
+ display: flex;
929
+ align-items: center;
930
+ justify-content: center;
931
+ color: var(--tfo-btn-icon-color);
932
+ transition: all 0.3s;
933
+ }
934
+
935
+ .tfo-action-btn span {
936
+ font-size: 0.8rem;
937
+ font-weight: 700;
938
+ }
939
+
940
+ .tfo-action-btn:hover {
941
+ border-color: var(--tfo-pink);
942
+ transform: translateY(-4px);
943
+ box-shadow: 0 10px 20px rgba(253, 41, 123, 0.1);
944
+ }
945
+
946
+ .tfo-action-btn:hover .tfo-btn-icon {
947
+ background: #fff5f7;
948
+ color: var(--tfo-pink);
949
+ }
950
+
951
+ :global(.theme-dark) .tfo-action-btn:hover .tfo-btn-icon {
952
+ background: rgba(253, 41, 123, 0.15);
953
+ color: var(--tfo-pink);
954
+ }
955
+
956
+ .tfo-action-btn.is-active {
957
+ background: #fff5f7;
958
+ border-color: var(--tfo-pink);
959
+ color: var(--tfo-pink);
960
+ }
961
+
962
+ :global(.theme-dark) .tfo-action-btn.is-active {
963
+ background: rgba(253, 41, 123, 0.1);
964
+ }
965
+
966
+ .tfo-action-btn.is-active .tfo-btn-icon {
967
+ background: var(--tfo-pink);
968
+ color: white;
969
+ }
970
+
971
+ .tfo-export-desc {
972
+ font-size: 11px;
973
+ margin-bottom: 1rem;
974
+ color: var(--tfo-muted);
975
+ }
976
+
977
+ .tfo-export-btn {
978
+ width: 100%;
979
+ padding: 1.2rem;
980
+ background: var(--tfo-gradient);
981
+ border: none;
982
+ border-radius: 16px;
983
+ color: white;
984
+ font-weight: 800;
985
+ font-size: 1.05rem;
986
+ cursor: pointer;
987
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
988
+ box-shadow: 0 10px 25px rgba(253, 41, 123, 0.2);
989
+ }
990
+
991
+ .tfo-export-btn:hover {
992
+ transform: translateY(-3px);
993
+ box-shadow: 0 15px 35px rgba(253, 41, 123, 0.35);
994
+ filter: brightness(1.05);
995
+ }
996
+
997
+ .tfo-analysis-board {
998
+ display: flex;
999
+ flex-direction: column;
1000
+ gap: 1rem;
1001
+ margin-top: 2rem;
1002
+ width: 100%;
1003
+ }
1004
+
1005
+ .tfo-analysis-col {
1006
+ display: none;
1007
+ background: var(--tfo-analysis-bg);
1008
+ border-radius: 20px;
1009
+ padding: 1rem;
1010
+ border: 1px solid rgba(0, 0, 0, 0.05);
1011
+ flex-direction: column;
1012
+ gap: 0.5rem;
1013
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
1014
+ }
1015
+
1016
+ :global(.theme-dark) .tfo-analysis-col {
1017
+ border-color: rgba(255, 255, 255, 0.06);
1018
+ }
1019
+
1020
+ .tfo-col-header {
1021
+ display: flex;
1022
+ align-items: center;
1023
+ gap: 0.6rem;
1024
+ font-size: 0.75rem;
1025
+ font-weight: 800;
1026
+ text-transform: uppercase;
1027
+ letter-spacing: 0.5px;
1028
+ opacity: 0.7;
1029
+ margin-bottom: 0.2rem;
1030
+ }
1031
+
1032
+ .tfo-col-icon {
1033
+ width: 18px;
1034
+ height: 18px;
1035
+ display: flex;
1036
+ align-items: center;
1037
+ justify-content: center;
1038
+ }
1039
+
1040
+ .tfo-analysis-col--success .tfo-col-header { color: #10b981; }
1041
+ .tfo-analysis-col--warning .tfo-col-header { color: #f59e0b; }
1042
+ .tfo-analysis-col--error .tfo-col-header { color: #ef4444; }
1043
+
1044
+ .tfo-alerts-stack {
1045
+ display: flex;
1046
+ flex-direction: column;
1047
+ gap: 0.5rem;
1048
+ }
1049
+
1050
+ .tfo-alert {
1051
+ display: flex;
1052
+ align-items: flex-start;
1053
+ gap: 0.8rem;
1054
+ padding: 0.6rem 0;
1055
+ font-size: 0.85rem;
1056
+ font-weight: 600;
1057
+ color: var(--tfo-alert-color);
1058
+ border-bottom: 1px solid rgba(0, 0, 0, 0.03);
1059
+ line-height: 1.4;
1060
+ overflow-wrap: break-word;
1061
+ }
1062
+
1063
+ :global(.theme-dark) .tfo-alert {
1064
+ border-bottom-color: rgba(255, 255, 255, 0.04);
1065
+ }
1066
+
1067
+ .tfo-alert:last-child { border-bottom: none; }
1068
+
1069
+ .tfo-alert::before {
1070
+ content: '';
1071
+ margin-top: 0.6rem;
1072
+ width: 8px;
1073
+ height: 8px;
1074
+ border-radius: 50%;
1075
+ flex-shrink: 0;
1076
+ }
1077
+
1078
+ .tfo-alert--success::before {
1079
+ background: #10b981;
1080
+ box-shadow: 0 0 10px rgba(16, 185, 129, 0.4);
1081
+ }
1082
+
1083
+ .tfo-alert--warning::before {
1084
+ background: #f59e0b;
1085
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.4);
1086
+ }
1087
+
1088
+ .tfo-alert--error::before {
1089
+ background: #ef4444;
1090
+ box-shadow: 0 0 10px rgba(239, 68, 68, 0.4);
1091
+ }
1092
+
1093
+ .tfo-ai-loader {
1094
+ position: absolute;
1095
+ inset: 0;
1096
+ background: rgba(255, 255, 255, 0.85);
1097
+ backdrop-filter: blur(12px);
1098
+ z-index: 5;
1099
+ display: flex;
1100
+ align-items: center;
1101
+ justify-content: center;
1102
+ border-radius: 40px;
1103
+ }
1104
+
1105
+ :global(.theme-dark) .tfo-ai-loader {
1106
+ background: rgba(15, 23, 42, 0.85);
1107
+ }
1108
+
1109
+ .tfo-loader-content {
1110
+ display: flex;
1111
+ flex-direction: column;
1112
+ align-items: center;
1113
+ gap: 1.5rem;
1114
+ color: var(--tfo-pink);
1115
+ }
1116
+
1117
+ .tfo-scanner-bar {
1118
+ position: absolute;
1119
+ top: 0;
1120
+ left: 0;
1121
+ width: 100%;
1122
+ height: 4px;
1123
+ background: var(--tfo-gradient);
1124
+ box-shadow: 0 0 20px var(--tfo-pink);
1125
+ animation: tfo-scan 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
1126
+ z-index: 1001;
1127
+ }
1128
+
1129
+ @keyframes tfo-scan {
1130
+ 0% { top: 10%; }
1131
+ 50% { top: 90%; }
1132
+ 100% { top: 10%; }
1133
+ }
1134
+
1135
+ .tfo-hidden {
1136
+ display: none;
1137
+ }
1138
+
1139
+ .tfo-device {
1140
+ width: 100%;
1141
+ aspect-ratio: 9/19;
1142
+ background: #fff;
1143
+ display: flex;
1144
+ flex-direction: column;
1145
+ position: relative;
1146
+ user-select: none;
1147
+ border-radius: 54px;
1148
+ border: 14px solid #1a1a1a;
1149
+ box-shadow: 0 50px 100px rgba(0, 0, 0, 0.15);
1150
+ overflow: hidden;
1151
+ }
1152
+
1153
+ @media (max-width: 640px) {
1154
+ .tfo-device {
1155
+ border-width: 8px;
1156
+ border-radius: 36px;
1157
+ }
1158
+
1159
+ .tfo-status-bar {
1160
+ height: 34px;
1161
+ padding: 0 15px;
1162
+ }
1163
+
1164
+ .tfo-dynamic-island {
1165
+ width: 80px;
1166
+ height: 20px;
1167
+ }
1168
+
1169
+ .tfo-app-header {
1170
+ height: 44px;
1171
+ padding: 0 15px;
1172
+ }
1173
+
1174
+ .tfo-card-info {
1175
+ padding-bottom: 15px;
1176
+ }
1177
+
1178
+ .tfo-name {
1179
+ font-size: 22px;
1180
+ }
1181
+
1182
+ .tfo-job {
1183
+ font-size: 14px;
1184
+ }
1185
+
1186
+ .tfo-tags {
1187
+ font-size: 10px;
1188
+ }
1189
+ }
1190
+
1191
+ .tfo-status-bar {
1192
+ height: 44px;
1193
+ padding: 0 20px;
1194
+ display: flex;
1195
+ justify-content: space-between;
1196
+ align-items: center;
1197
+ font-size: 14px;
1198
+ font-weight: 600;
1199
+ color: #1a1a1a;
1200
+ }
1201
+
1202
+ .tfo-dynamic-island {
1203
+ width: 100px;
1204
+ height: 25px;
1205
+ background: #000;
1206
+ border-radius: 20px;
1207
+ }
1208
+
1209
+ .tfo-status-icons {
1210
+ display: flex;
1211
+ gap: 6px;
1212
+ }
1213
+
1214
+ .tfo-app-header {
1215
+ height: 50px;
1216
+ display: flex;
1217
+ justify-content: space-between;
1218
+ align-items: center;
1219
+ padding: 0 20px;
1220
+ border-bottom: 1px solid #f1f5f9;
1221
+ background: #fff;
1222
+ }
1223
+
1224
+ .tfo-logo {
1225
+ flex: 1;
1226
+ display: flex;
1227
+ justify-content: center;
1228
+ }
1229
+
1230
+ .tfo-icon-placeholder, .tfo-settings-icon {
1231
+ width: 40px;
1232
+ height: 40px;
1233
+ display: flex;
1234
+ align-items: center;
1235
+ justify-content: center;
1236
+ color: #cbd5e1;
1237
+ }
1238
+
1239
+ .tfo-card-container {
1240
+ flex: 1;
1241
+ padding: 12px;
1242
+ display: flex;
1243
+ align-items: center;
1244
+ justify-content: center;
1245
+ background: #fff;
1246
+ }
1247
+
1248
+ .tfo-main-card {
1249
+ position: relative;
1250
+ width: 100%;
1251
+ height: 100%;
1252
+ border-radius: 24px;
1253
+ overflow: hidden;
1254
+ background: #000;
1255
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
1256
+ }
1257
+
1258
+ .tfo-canvas {
1259
+ width: 100%;
1260
+ height: 100%;
1261
+ object-fit: cover;
1262
+ }
1263
+
1264
+ .tfo-card-overlay {
1265
+ position: absolute;
1266
+ inset: 0;
1267
+ display: flex;
1268
+ flex-direction: column;
1269
+ justify-content: space-between;
1270
+ padding: 12px;
1271
+ pointer-events: none;
1272
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 40%),
1273
+ linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, transparent 15%);
1274
+ }
1275
+
1276
+ .tfo-progress {
1277
+ display: flex;
1278
+ gap: 4px;
1279
+ }
1280
+
1281
+ .tfo-segment {
1282
+ flex: 1;
1283
+ height: 3px;
1284
+ background: rgba(255, 255, 255, 0.4);
1285
+ border-radius: 2px;
1286
+ }
1287
+
1288
+ .tfo-segment--active { background: #fff; }
1289
+
1290
+ .tfo-card-info {
1291
+ color: #fff;
1292
+ padding-bottom: 20px;
1293
+ z-index: 20;
1294
+ }
1295
+
1296
+ .tfo-primary-info {
1297
+ display: flex;
1298
+ align-items: center;
1299
+ gap: 8px;
1300
+ margin-bottom: 4px;
1301
+ }
1302
+
1303
+ .tfo-badge {
1304
+ width: 20px;
1305
+ height: 20px;
1306
+ background: #1da1f2;
1307
+ border-radius: 50%;
1308
+ display: flex;
1309
+ align-items: center;
1310
+ justify-content: center;
1311
+ }
1312
+
1313
+ .tfo-name {
1314
+ font-size: 28px;
1315
+ font-weight: 800;
1316
+ margin: 0;
1317
+ }
1318
+
1319
+ .tfo-secondary-info {
1320
+ display: flex;
1321
+ flex-direction: column;
1322
+ gap: 4px;
1323
+ }
1324
+
1325
+ .tfo-job {
1326
+ font-size: 16px;
1327
+ font-weight: 600;
1328
+ }
1329
+
1330
+ .tfo-tags {
1331
+ display: flex;
1332
+ gap: 12px;
1333
+ font-size: 12px;
1334
+ font-weight: 500;
1335
+ opacity: 0.9;
1336
+ }
1337
+
1338
+ .tfo-tag {
1339
+ display: flex;
1340
+ align-items: center;
1341
+ gap: 4px;
1342
+ }
1343
+
1344
+ .tfo-device-actions {
1345
+ height: 100px;
1346
+ display: flex;
1347
+ justify-content: space-evenly;
1348
+ align-items: center;
1349
+ padding: 0 10px;
1350
+ background: #fff;
1351
+ }
1352
+
1353
+ .tfo-btn-circular {
1354
+ width: 44px;
1355
+ height: 44px;
1356
+ border-radius: 50%;
1357
+ background: #fff;
1358
+ border: none;
1359
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
1360
+ display: flex;
1361
+ align-items: center;
1362
+ justify-content: center;
1363
+ cursor: pointer;
1364
+ transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
1365
+ }
1366
+
1367
+ .tfo-btn-nope, .tfo-btn-like {
1368
+ width: 54px;
1369
+ height: 54px;
1370
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);
1371
+ }
1372
+
1373
+ .tfo-btn-circular:active { transform: scale(0.92); }
1374
+ .tfo-btn-circular:hover {
1375
+ transform: translateY(-2px);
1376
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
1377
+ }
1378
+
1379
+ .tfo-nav-bar {
1380
+ height: 60px;
1381
+ display: flex;
1382
+ justify-content: space-evenly;
1383
+ align-items: center;
1384
+ border-top: 1px solid #f1f5f9;
1385
+ color: #94a3b8;
1386
+ background: #fff;
1387
+ }
1388
+
1389
+ .tfo-nav-item--active { color: #FD297B; }
1390
+
1391
+ .tfo-grid-overlay {
1392
+ position: absolute;
1393
+ inset: 0;
1394
+ display: grid;
1395
+ grid-template-columns: repeat(3, 1fr);
1396
+ grid-template-rows: repeat(3, 1fr);
1397
+ pointer-events: none;
1398
+ z-index: 1;
1399
+ }
1400
+
1401
+ .tfo-grid-overlay div {
1402
+ border: 0.5px solid rgba(255, 255, 255, 0.3);
1403
+ }
1404
+
1405
+ .tfo-deadzones {
1406
+ position: absolute;
1407
+ inset: 0;
1408
+ pointer-events: none;
1409
+ z-index: 2;
1410
+ }
1411
+
1412
+ .tfo-deadzone {
1413
+ position: absolute;
1414
+ left: 0;
1415
+ width: 100%;
1416
+ background: rgba(255, 0, 0, 0.2);
1417
+ border: 2px dashed rgba(255, 0, 0, 0.5);
1418
+ display: flex;
1419
+ align-items: center;
1420
+ justify-content: center;
1421
+ color: #fff;
1422
+ font-size: 10px;
1423
+ font-weight: 700;
1424
+ text-transform: uppercase;
1425
+ }
1426
+
1427
+ .tfo-deadzone--top {
1428
+ top: 0;
1429
+ height: 15%;
1430
+ }
1431
+
1432
+ .tfo-deadzone--bottom {
1433
+ bottom: 0;
1434
+ height: 28%;
1435
+ }
1436
+
1437
+ .tfo-deadzone span {
1438
+ background: #ff4458;
1439
+ padding: 2px 6px;
1440
+ border-radius: 4px;
1441
+ }
1442
+
1443
+ .tfo-reset-btn {
1444
+ background: none;
1445
+ border: none;
1446
+ color: #94a3b8;
1447
+ cursor: pointer;
1448
+ padding: 8px;
1449
+ border-radius: 50%;
1450
+ transition: all 0.3s ease;
1451
+ display: flex;
1452
+ align-items: center;
1453
+ justify-content: center;
1454
+ }
1455
+
1456
+ .tfo-reset-btn:hover {
1457
+ background: #f1f5f9;
1458
+ color: var(--tfo-pink);
1459
+ }
1460
+
1461
+ .tfo-upload-overlay {
1462
+ position: absolute;
1463
+ inset: 0;
1464
+ background: #0a0a0a;
1465
+ display: flex;
1466
+ flex-direction: column;
1467
+ align-items: center;
1468
+ justify-content: center;
1469
+ color: white;
1470
+ cursor: pointer;
1471
+ z-index: 3;
1472
+ transition: all 0.4s ease;
1473
+ padding: 2rem;
1474
+ text-align: center;
1475
+ }
1476
+
1477
+ .tfo-upload-overlay.tfo-hidden {
1478
+ opacity: 0;
1479
+ pointer-events: none;
1480
+ transform: scale(1.1);
1481
+ }
1482
+
1483
+ .tfo-upload-circle {
1484
+ width: 80px;
1485
+ height: 80px;
1486
+ background: rgba(255, 255, 255, 0.05);
1487
+ border: 2px dashed rgba(255, 255, 255, 0.2);
1488
+ border-radius: 50%;
1489
+ display: flex;
1490
+ align-items: center;
1491
+ justify-content: center;
1492
+ margin-bottom: 1.5rem;
1493
+ transition: all 0.3s ease;
1494
+ }
1495
+
1496
+ .tfo-main-card:hover .tfo-upload-circle {
1497
+ background: rgba(253, 41, 123, 0.1);
1498
+ border-color: var(--tfo-pink);
1499
+ color: var(--tfo-pink);
1500
+ transform: translateY(-5px);
1501
+ }
1502
+
1503
+ .tfo-upload-overlay h3 {
1504
+ margin: 0;
1505
+ font-size: 1.2rem;
1506
+ font-weight: 700;
1507
+ }
1508
+
1509
+ .tfo-upload-overlay p {
1510
+ margin: 0.5rem 0 0;
1511
+ font-size: 0.9rem;
1512
+ opacity: 0.5;
1513
+ }
1514
+ </style>