webs-sdk 0.19.64 → 0.19.65

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 (100) hide show
  1. package/AGENTS.md +44 -0
  2. package/CLAUDE.md +80 -0
  3. package/README.md +513 -490
  4. package/dist/components/ARFilters/ARFilters.js +54 -54
  5. package/dist/components/ArtGenerator.js +141 -141
  6. package/dist/components/AvatarAI.js +14 -14
  7. package/dist/components/CookieConsent/constants.js +31 -31
  8. package/dist/components/CreativeFaceSwap.js +23 -23
  9. package/dist/components/GifsIa.js +76 -76
  10. package/dist/components/MemeGenerator.js +12 -12
  11. package/dist/components/PasswordReset/constants.js +33 -33
  12. package/dist/components/RecoveryPassword/constants.js +27 -27
  13. package/dist/components/SpinningWheel/SpinningWheel.js +7 -7
  14. package/dist/components/SpinningWheel/index.d.ts +1 -1
  15. package/dist/components/SpinningWheel/index.d.ts.map +1 -1
  16. package/dist/components/SpinningWheel/index.js +2 -1
  17. package/dist/components/SpinningWheel/index.js.map +1 -1
  18. package/dist/components/ui/ConfiguredMediaPlayer.d.ts +49 -0
  19. package/dist/components/ui/ConfiguredMediaPlayer.d.ts.map +1 -0
  20. package/dist/components/ui/ConfiguredMediaPlayer.js +111 -0
  21. package/dist/components/ui/ConfiguredMediaPlayer.js.map +1 -0
  22. package/dist/components/ui/badge.d.ts +1 -0
  23. package/dist/components/ui/badge.d.ts.map +1 -1
  24. package/dist/components/ui/badge.js +1 -1
  25. package/dist/components/ui/badge.js.map +1 -1
  26. package/dist/components/ui/compose-refs.d.ts +6 -0
  27. package/dist/components/ui/compose-refs.d.ts.map +1 -0
  28. package/dist/components/ui/compose-refs.js +57 -0
  29. package/dist/components/ui/compose-refs.js.map +1 -0
  30. package/dist/components/ui/dropdown-menu.d.ts +3 -1
  31. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  32. package/dist/components/ui/dropdown-menu.js +2 -2
  33. package/dist/components/ui/dropdown-menu.js.map +1 -1
  34. package/dist/components/ui/index.d.ts +2 -0
  35. package/dist/components/ui/index.d.ts.map +1 -1
  36. package/dist/components/ui/index.js +2 -0
  37. package/dist/components/ui/index.js.map +1 -1
  38. package/dist/components/ui/media-player.d.ts +127 -0
  39. package/dist/components/ui/media-player.d.ts.map +1 -0
  40. package/dist/components/ui/media-player.js +1633 -0
  41. package/dist/components/ui/media-player.js.map +1 -0
  42. package/dist/components/ui/tooltip.d.ts +3 -1
  43. package/dist/components/ui/tooltip.d.ts.map +1 -1
  44. package/dist/components/ui/tooltip.js +2 -2
  45. package/dist/components/ui/tooltip.js.map +1 -1
  46. package/dist/components/ui/use-lazy-ref.d.ts +4 -0
  47. package/dist/components/ui/use-lazy-ref.d.ts.map +1 -0
  48. package/dist/components/ui/use-lazy-ref.js +45 -0
  49. package/dist/components/ui/use-lazy-ref.js.map +1 -0
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +4 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/network.d.ts +12 -0
  55. package/dist/network.d.ts.map +1 -0
  56. package/dist/network.js +15 -0
  57. package/dist/network.js.map +1 -0
  58. package/dist/server.d.ts +54 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/server.js +74 -0
  61. package/dist/server.js.map +1 -0
  62. package/dist/types/media-chrome-store-types.d.ts +107 -0
  63. package/dist/types/media-chrome-store-types.d.ts.map +1 -0
  64. package/dist/types/media-chrome-store-types.js +3 -0
  65. package/dist/types/media-chrome-store-types.js.map +1 -0
  66. package/dist/utils/QRCode.d.ts +1 -0
  67. package/dist/utils/QRCode.d.ts.map +1 -0
  68. package/dist/utils/QRCode.js +2 -0
  69. package/dist/utils/QRCode.js.map +1 -0
  70. package/dist/utils/imageValidation.d.ts +6 -0
  71. package/dist/utils/imageValidation.d.ts.map +1 -0
  72. package/dist/utils/imageValidation.js +64 -0
  73. package/dist/utils/imageValidation.js.map +1 -0
  74. package/docs/CONFIGURED_MEDIA_PLAYER.md +40 -0
  75. package/docs/CORP_AUTH_USAGE.md +687 -0
  76. package/docs/EXAMPLES_WALLPAPERSNAME_IMPLEMENTATION.md +1445 -0
  77. package/docs/EXAMPLE_ENDPOINTS_COMUNICATION.md +55 -0
  78. package/docs/EXTERNAL_SDK_GUIDE.md +1340 -0
  79. package/docs/FOR_YOU_360_FACESWAP_FIX.md +400 -0
  80. package/docs/FOR_YOU_IMPLEMENTATION_GUIDE.md +1413 -0
  81. package/docs/FOR_YOU_QUIZ_ARCHITECTURE.md +753 -0
  82. package/docs/FOR_YOU_SDK_WALLPAPERSNAME_BUG.md +106 -0
  83. package/docs/FOR_YOU_WALLPAPERSNAME_ENHANCEMENT_PLAN.md +210 -0
  84. package/docs/LEGAL_USAGE.md +190 -0
  85. package/docs/STREAK_IMPLEMENTATION_GUIDE.md +430 -0
  86. package/package.json +106 -84
  87. package/public/spin2win/assets/border-afristream.svg +26 -26
  88. package/public/spin2win/assets/border-efc.svg +23 -23
  89. package/public/spin2win/assets/border-streetreel.svg +23 -23
  90. package/public/spin2win/assets/border-totalgym.svg +27 -27
  91. package/public/spin2win/assets/border-womantoday.svg +26 -26
  92. package/public/spin2win/assets/indicator-afristream.svg +28 -28
  93. package/public/spin2win/assets/indicator-efc.svg +29 -29
  94. package/public/spin2win/assets/indicator-streetreel.svg +29 -29
  95. package/public/spin2win/assets/indicator-totalgym.svg +29 -29
  96. package/public/spin2win/fonts/switzer/README.md +22 -22
  97. package/scripts/install-agent-skill.cjs +34 -0
  98. package/skills/webs-sdk-package/SKILL.md +11 -0
  99. package/skills/webs-sdk-package/references/components.md +30 -0
  100. package/skills/webs-sdk-package/references/integration.md +32 -0
@@ -0,0 +1,1413 @@
1
+ # ForYou — Guía de implementación del SDK
2
+
3
+ > **Versión SDK:** 0.18.83+
4
+ > **Componente principal:** `ForYouPage`
5
+
6
+ ---
7
+
8
+ ## Índice
9
+
10
+ 1. [Concepto](#concepto)
11
+ 2. [Sistema anterior vs. sistema nuevo](#sistema-anterior-vs-sistema-nuevo)
12
+ 3. [Instalación](#instalación)
13
+ 4. [Uso básico — ForYouPage](#uso-básico--foryoupage)
14
+ 5. [Internacionalización (locale)](#internacionalización-locale)
15
+ 6. [Navegación](#navegación)
16
+ 7. [Endpoints — modos de llamada a API](#endpoints--modos-de-llamada-a-api)
17
+ 8. [Referencia de props: ForYouPage](#referencia-de-props-foryoupage)
18
+ 9. [Referencia de props: ForYouFeatureConfig](#referencia-de-props-foryoufeatureconfig)
19
+ 10. [Referencia de props: ForYouCardVisual](#referencia-de-props-foryoucardvisual)
20
+ 11. [Referencia de props: ForYouPageTheme](#referencia-de-props-foryoupagetheme)
21
+ 12. [forYouFetch — helper de API](#foryoufetch--helper-de-api)
22
+ 13. [Features disponibles](#features-disponibles)
23
+ 14. [Uso de hooks fuera de ForYouPage](#uso-de-hooks-fuera-de-foryoupage)
24
+ 15. [Ejemplo completo Winsports](#ejemplo-completo-winsports)
25
+ 16. [Descargas y CORS — Proxy de medios](#descargas-y-cors--proxy-de-medios)
26
+ 17. [Feature: CreativeFaceSwap — ejemplos + endpoints](#feature-creativefaceswap--ejemplos--endpoints)
27
+ 18. [Migración desde el sistema anterior](#migración-desde-el-sistema-anterior)
28
+
29
+ ---
30
+
31
+ ## Concepto
32
+
33
+ `ForYouPage` es el único componente que necesitas importar para montar la sección completa de ForYou (Fanzone / Para Ti) en cualquier portal.
34
+
35
+ **Antes (sistema antiguo):** importabas componente a componente y construías la página en el proyecto.
36
+ **Ahora (sistema nuevo):** un único import, toda la lógica configurada vía props.
37
+
38
+ Ambos sistemas son **100% compatibles y coexistentes**. Puedes usar el sistema anterior en paralelo al nuevo sin conflicto.
39
+
40
+ ---
41
+
42
+ ## Sistema anterior vs. sistema nuevo
43
+
44
+ ```tsx
45
+ // ─── SISTEMA ANTERIOR ─────────────────────────────────────────────────────────
46
+ // Una feature = un import + su propia página con navegación manual
47
+
48
+ import { Wallpapers, Quiz, Ringtone } from 'webs-sdk'
49
+
50
+ function ForYouPage() {
51
+ const [active, setActive] = useState<string | null>(null)
52
+ // ...construir navegación, botones de volver, breadcrumbs, etc.
53
+ }
54
+
55
+ // ─── SISTEMA NUEVO ────────────────────────────────────────────────────────────
56
+ // Un único import, toda la página configurada vía props
57
+
58
+ import { ForYouPage } from 'webs-sdk'
59
+
60
+ function MyForYouPage() {
61
+ return (
62
+ <ForYouPage
63
+ locale="es"
64
+ features={{
65
+ wallpapers: { card: { desktopImage: '/img/wallpapers.jpg' } },
66
+ quiz: { card: { desktopImage: '/img/quiz.jpg' } },
67
+ ringtone: { card: { desktopImage: '/img/ringtone.jpg' } },
68
+ }}
69
+ />
70
+ )
71
+ }
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Instalación
77
+
78
+ ```bash
79
+ npm install webs-sdk
80
+ # o
81
+ yarn add webs-sdk
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Uso básico — ForYouPage
87
+
88
+ ```tsx
89
+ import { ForYouPage } from 'webs-sdk'
90
+
91
+ export default function ForYouSection() {
92
+ return (
93
+ <ForYouPage
94
+ locale="es"
95
+ features={{
96
+ ringtone: {
97
+ card: {
98
+ desktopImage: '/images/ringtone-desktop.jpg',
99
+ mobileImage: '/images/ringtone-mobile.jpg',
100
+ ctaColor: '#FF5500',
101
+ backgroundColor: '#F9F9F9',
102
+ },
103
+ config: { tags: 'ringtone' },
104
+ },
105
+ wallpapers: {
106
+ card: {
107
+ desktopImage: '/images/wallpapers-desktop.jpg',
108
+ imageOnRight: false,
109
+ ctaColor: '#E10600',
110
+ },
111
+ config: { tags: 'wallpaper' },
112
+ },
113
+ quiz: {
114
+ card: {
115
+ desktopImage: '/images/quiz-desktop.jpg',
116
+ ctaColor: '#0066CC',
117
+ },
118
+ config: { tags: 'quiz' },
119
+ },
120
+ }}
121
+ />
122
+ )
123
+ }
124
+ ```
125
+
126
+ **¿Qué hace esto?**
127
+ - Muestra un **hub** con las cards de cada feature.
128
+ - Al pulsar una card, navega internamente a la vista del componente.
129
+ - Botón "Volver" con icono SVG para regresar al hub.
130
+ - Scroll automático al tope al entrar/salir.
131
+ - URL actualizada con `?feature=<key>` para deep-linking.
132
+
133
+ ---
134
+
135
+ ## Internacionalización (locale)
136
+
137
+ El SDK incluye traducciones para **8 idiomas**:
138
+
139
+ | Código | Idioma | RTL |
140
+ |--------|--------|-----|
141
+ | `en` | English (default) | No |
142
+ | `es` | Español | No |
143
+ | `fr` | Français | No |
144
+ | `pl` | Polski | No |
145
+ | `nl` | Nederlands | No |
146
+ | `tr` | Türkçe | No |
147
+ | `ro` | Română | No |
148
+ | `ar` | العربية | **Sí** |
149
+
150
+ ```tsx
151
+ // Solo pasar el locale — el SDK usa sus propias traducciones
152
+ <ForYouPage locale="es" features={...} />
153
+
154
+ // Override puntual — solo los textos que necesites cambiar
155
+ <ForYouPage
156
+ locale="es"
157
+ translations={{
158
+ backButtonText: '← Atrás',
159
+ cards: {
160
+ ringtone: { title: 'Mis Tonos', ctaText: 'Ver más' },
161
+ },
162
+ }}
163
+ features={...}
164
+ />
165
+ ```
166
+
167
+ **Árabe (RTL):** al pasar `locale="ar"`, el SDK añade automáticamente `dir="rtl"` al contenedor raíz.
168
+
169
+ ### Lógica de merge
170
+
171
+ ```
172
+ SDK defaults 'en' → SDK defaults [locale] → translations (proyecto)
173
+ ```
174
+
175
+ Los textos del proyecto **siempre tienen prioridad** sobre los del SDK.
176
+
177
+ ---
178
+
179
+ ## Navegación
180
+
181
+ `ForYouPage` gestiona toda la navegación internamente:
182
+
183
+ - **`?feature=<key>` en la URL** — se actualiza con `window.history.pushState` al entrar/salir. Permite deep-linking directo (p.ej.: `/fanzone?feature=quiz` abre el quiz directamente).
184
+ - **Botón "Atrás" del navegador** — listener `popstate` que vuelve al hub o a la feature anterior.
185
+ - **Scroll automático** — `containerRef.scrollIntoView()` al cambiar de vista.
186
+ - **`initialFeature` prop** — para arrancar directamente en una feature desde el código.
187
+ - **`hideBreadcrumb` en Wallpapers** — cuando Wallpapers se renderiza dentro de ForYouPage, su breadcrumb interno se oculta automáticamente (el hub ya proporciona navegación).
188
+
189
+ ```tsx
190
+ // Abrir directamente en quiz (p.ej. desde un banner externo)
191
+ <ForYouPage initialFeature="quiz" features={...} />
192
+
193
+ // O leer el query param manualmente y pasarlo
194
+ const feature = new URLSearchParams(location.search).get('feature')
195
+ <ForYouPage initialFeature={feature as keyof ForYouFeatures} features={...} />
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Endpoints — modos de llamada a API
201
+
202
+ ### Modo 1: URL directa (sistema anterior — compatible)
203
+
204
+ ```ts
205
+ config: {
206
+ apiUrl: '/api/get_contents_by_tags',
207
+ tags: 'quiz',
208
+ locale: 'es',
209
+ }
210
+ ```
211
+
212
+ ### Modo 2: apiBaseUrl (SDK construye la URL)
213
+
214
+ ```ts
215
+ config: {
216
+ apiBaseUrl: 'https://api.winsports.co',
217
+ tags: ['quizws', 'quiz'],
218
+ locale: 'es',
219
+ }
220
+ ```
221
+
222
+ El SDK mapea el preset `getGlobalContentsByTags` → `/api/get_contents_by_tags`.
223
+
224
+ ### Modo 3: Multi-preset (múltiples llamadas en paralelo)
225
+
226
+ ```ts
227
+ config: {
228
+ apiBaseUrl: 'https://api.winsports.co',
229
+ locale: 'es',
230
+ contents: [
231
+ {
232
+ preset: 'getGlobalContentsByIDs',
233
+ includeIds: [103433, 103420, 103396, 103371],
234
+ },
235
+ {
236
+ preset: 'getGlobalContentsByTags',
237
+ tags: ['quizws'],
238
+ },
239
+ ],
240
+ }
241
+ ```
242
+
243
+ **Presets disponibles:**
244
+
245
+ | Preset | Endpoint |
246
+ |--------|----------|
247
+ | `getGlobalContentsByTags` | `/api/get_contents_by_tags` |
248
+ | `getGlobalContentsByIDs` | `/api/get_contents_by_tags` (con `include_ids`) |
249
+ | `getContentsByCategory` | `/api/get_global_api_contents_by_category` |
250
+ | `getCollectionByTags` | `/api/get_global_api_collections_by_tags` |
251
+ | `getContentPreset` | `/api/content-preset` |
252
+
253
+ Los resultados de múltiples llamadas se fusionan y **deduplicados por `id`**.
254
+
255
+ ---
256
+
257
+ ## Referencia de props: ForYouPage
258
+
259
+ | Prop | Tipo | Default | Descripción |
260
+ |------|------|---------|-------------|
261
+ | `features` | `ForYouFeatures` | **requerido** | Features a mostrar. Cada key con `card` aparece en el hub. |
262
+ | `order` | `Array<keyof ForYouFeatures>` | Orden del objeto | Orden de renderizado de las cards en el hub. |
263
+ | `hubBanner` | `ForYouHubBanner` | - | Banner hero opcional encima de las cards. |
264
+ | `locale` | `ForYouLocale` | `'en'` | Idioma del SDK. |
265
+ | `translations` | `ForYouPageTranslations` | SDK defaults | Override de textos sobre el locale. |
266
+ | `pageTheme` | `ForYouPageTheme` | - | Tema visual de la página. |
267
+ | `initialFeature` | `keyof ForYouFeatures` | - | Feature que se abre al montar. |
268
+
269
+ ---
270
+
271
+ ## Referencia de props: WallpapersFeatureOverride — tabs
272
+
273
+ El feature `wallpapers` soporta un selector de pestañas opcionales para filtrar contenido por categoría (como en Winsports). Se activa añadiendo `tabs` a la configuración del feature.
274
+
275
+ | Prop | Tipo | Descripción |
276
+ |------|------|-------------|
277
+ | `tabs` | `WallpapersTab[]` | Array de pestañas. Si se define, aparece el selector encima del componente. |
278
+ | `tabsTheme` | `object` | Personalización visual del selector de tabs (ver tabla abajo). |
279
+
280
+ ### WallpapersTab
281
+
282
+ | Prop | Tipo | Descripción |
283
+ |------|------|-------------|
284
+ | `label` | `string` | Texto visible en la pestaña. |
285
+ | `tag` | `string` | Tag de API para filtrar los wallpapers de esta categoría. |
286
+
287
+ ### tabsTheme
288
+
289
+ | Prop | Tipo | Default | Descripción |
290
+ |------|------|---------|-------------|
291
+ | `activeColor` | `string` | `'#001B5E'` | Fondo de la tab activa. |
292
+ | `activeTextColor` | `string` | `'#FFFFFF'` | Texto de la tab activa. |
293
+ | `inactiveColor` | `string` | `'transparent'` | Fondo de la tab inactiva. |
294
+ | `inactiveTextColor` | `string` | `'#001B5E'` | Texto de la tab inactiva. |
295
+ | `borderColor` | `string` | `'#E5E7EB'` | Color del borde inferior del selector. |
296
+
297
+ ### Ejemplo con tabs (estilo Winsports)
298
+
299
+ ```tsx
300
+ <ForYouPage
301
+ locale="es"
302
+ features={{
303
+ wallpapers: {
304
+ card: {
305
+ desktopImage: 'https://cdn.winsports.co/fanzone/wallpapers-card.jpg',
306
+ ctaColor: '#E6001E',
307
+ },
308
+ config: {
309
+ apiUrl: '/api/get_contents_by_tags',
310
+ locale: 'es',
311
+ website_id: '42',
312
+ heroBannerImage: 'https://cdn.winsports.co/fanzone/wallpapers-banner.jpg',
313
+ },
314
+ tabs: [
315
+ { label: 'Fútbol', tag: 'wallpaper-futbol' },
316
+ { label: 'Ciclismo', tag: 'wallpaper-ciclismo' },
317
+ { label: 'Béisbol', tag: 'wallpaper-beisbol' },
318
+ { label: 'Todos', tag: 'wallpaper-winsports' },
319
+ ],
320
+ tabsTheme: {
321
+ activeColor: '#E6001E',
322
+ activeTextColor: '#FFFFFF',
323
+ inactiveTextColor: '#333333',
324
+ borderColor: '#E5E7EB',
325
+ },
326
+ },
327
+ }}
328
+ />
329
+ ```
330
+
331
+ ### Sin tabs (por defecto)
332
+
333
+ Si no se define `tabs`, el componente muestra todos los wallpapers del tag configurado en `config.tags`:
334
+
335
+ ```tsx
336
+ wallpapers: {
337
+ card: { desktopImage: '/img/wallpapers.jpg' },
338
+ config: { apiUrl: '/api/get_contents_by_tags', tags: 'wallpaper' },
339
+ }
340
+ ```
341
+
342
+ ---
343
+
344
+ ## Referencia de props: ForYouHubBanner
345
+
346
+ Banner hero configurable que aparece sobre las cards en la vista hub. Completamente opcional.
347
+
348
+ | Prop | Tipo | Default | Descripción |
349
+ |------|------|---------|-------------|
350
+ | `desktopImage` | `string` | **requerido** | Imagen de fondo desktop. |
351
+ | `mobileImage` | `string` | = desktopImage | Imagen de fondo mobile. |
352
+ | `imageAlt` | `string` | `''` | Alt text de la imagen. |
353
+ | `title` | `string` | - | Título principal sobre la imagen. |
354
+ | `subtitle` | `string` | - | Subtítulo / descripción. |
355
+ | `backgroundColor` | `string` | `'transparent'` | Color de fondo (visible si no hay imagen). |
356
+ | `titleColor` | `string` | `'#FFFFFF'` | Color del título. |
357
+ | `subtitleColor` | `string` | `'#FFFFFF'` | Color del subtítulo. |
358
+ | `minHeight` | `string` | `'500px'` | Altura mínima del banner. |
359
+ | `padding` | `string` | `'40px 24px'` | Padding interno del texto. |
360
+ | `highlights` | `ForYouHubBannerHighlight[]` | - | Barra de highlights (iconos + texto) debajo del banner. |
361
+ | `highlightsBackgroundColor` | `string` | `'#FFFFFF'` | Fondo de la barra de highlights. |
362
+ | `highlightsTextColor` | `string` | `'#111827'` | Color del texto de highlights. |
363
+ | `highlightsIconColor` | `string` | = highlightsTextColor | Color del icono SVG de highlights. |
364
+
365
+ ### ForYouHubBannerHighlight
366
+
367
+ | Prop | Tipo | Descripción |
368
+ |------|------|-------------|
369
+ | `icon` | `string` | Raw SVG string (`<svg>...`) o URL de imagen. |
370
+ | `text` | `string` | Texto del highlight. |
371
+
372
+ ### Ejemplo con banner completo (estilo Winsports)
373
+
374
+ ```tsx
375
+ <ForYouPage
376
+ locale="es"
377
+ hubBanner={{
378
+ desktopImage: 'https://cdn.winsports.co/fanzone/hub-banner-desktop.jpg',
379
+ mobileImage: 'https://cdn.winsports.co/fanzone/hub-banner-mobile.jpg',
380
+ title: '¡Bienvenido a Para Ti!',
381
+ subtitle: 'El espacio de WinSports para personalizar tu móvil con el contenido deportivo más original.',
382
+ backgroundColor: '#1A1063',
383
+ titleColor: '#FFFFFF',
384
+ subtitleColor: '#FFFFFF',
385
+ minHeight: '520px',
386
+ highlights: [
387
+ {
388
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
389
+ text: 'Elige el mejor contenido para cada momento',
390
+ },
391
+ {
392
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>',
393
+ text: 'Despierta la creatividad que tienes dentro',
394
+ },
395
+ {
396
+ icon: '/icons/share.svg',
397
+ text: 'Guarda y comparte tus creaciones',
398
+ },
399
+ ],
400
+ highlightsBackgroundColor: '#FFFFFF',
401
+ highlightsTextColor: '#1A1063',
402
+ }}
403
+ features={WINSPORTS_FEATURES}
404
+ />
405
+ ```
406
+
407
+ ### Sin banner (por defecto)
408
+
409
+ Si no se pasa `hubBanner`, el hub muestra directamente las cards:
410
+
411
+ ```tsx
412
+ <ForYouPage locale="es" features={features} />
413
+ ```
414
+
415
+ ---
416
+
417
+ ## Referencia de props: ForYouFeatureConfig
418
+
419
+ Compartido por todas las features que hacen llamada a API:
420
+
421
+ | Prop | Tipo | Descripción |
422
+ |------|------|-------------|
423
+ | `apiUrl` | `string` | URL directa del endpoint (sistema anterior). |
424
+ | `apiBaseUrl` | `string` | Base URL. El SDK construye la URL según el preset. |
425
+ | `tags` | `string \| string[]` | Tags para la petición. |
426
+ | `contents` | `ForYouContentPreset[]` | Multi-preset: varias llamadas en paralelo. |
427
+ | `locale` | `string` | Locale para esta feature (override del global). |
428
+ | `website_id` | `string` | Website ID para la petición. |
429
+ | `initialCount` | `number` | Items visibles al inicio. |
430
+ | `loadMoreCount` | `number` | Items adicionales por "load more". |
431
+
432
+ ---
433
+
434
+ ## Referencia de props: ForYouCardVisual
435
+
436
+ Configuración visual de la card del hub (textos en `translations.cards.*`):
437
+
438
+ | Prop | Tipo | Default | Descripción |
439
+ |------|------|---------|-------------|
440
+ | `desktopImage` | `string` | **requerido** | Imagen desktop. |
441
+ | `mobileImage` | `string` | = desktopImage | Imagen mobile. |
442
+ | `imageAlt` | `string` | `''` | Alt text. |
443
+ | `imageOnRight` | `boolean` | `true` | Imagen a la derecha o izquierda. |
444
+ | `titleColor` | `string` | `'#001B5E'` | Color del título. |
445
+ | `descriptionColor` | `string` | `'#001B5E'` | Color de la descripción. |
446
+ | `ctaColor` | `string` | `'#FF5500'` | Color del botón. |
447
+ | `ctaHoverColor` | `string` | `'#ff1600'` | Color hover del botón. |
448
+ | `backgroundColor` | `string` | `'#F9F9F9'` | Color de fondo. |
449
+ | `paddingClass` | `string` | `'py-12 md:py-16 lg:py-20'` | Padding vertical (Tailwind). |
450
+
451
+ ---
452
+
453
+ ## Referencia de props: ForYouPageTheme
454
+
455
+ | Prop | Tipo | Default | Descripción |
456
+ |------|------|---------|-------------|
457
+ | `backgroundColor` | `string` | `'transparent'` | Fondo de la página. |
458
+ | `sectionSpacing` | `string` | `'0'` | Margen entre secciones del hub. |
459
+ | `backButtonStyle` | `React.CSSProperties` | - | Estilos inline del botón volver. |
460
+ | `backButtonClassName` | `string` | - | Clase CSS del botón volver. |
461
+ | `backButtonIconColor` | `string` | `'currentColor'` | Color del icono SVG. |
462
+ | `showBackButtonIcon` | `boolean` | `true` | Mostrar/ocultar icono SVG. |
463
+
464
+ ---
465
+
466
+ ## forYouFetch — helper de API
467
+
468
+ El helper `forYouFetch` se exporta independientemente para usarlo en proyectos sin `ForYouPage`:
469
+
470
+ ```ts
471
+ import { forYouFetch } from 'webs-sdk'
472
+
473
+ // Modo simple
474
+ const items = await forYouFetch({
475
+ apiUrl: '/api/get_contents_by_tags',
476
+ tags: 'quiz',
477
+ locale: 'es',
478
+ })
479
+
480
+ // Modo multi-preset
481
+ const items = await forYouFetch({
482
+ apiBaseUrl: 'https://api.winsports.co',
483
+ locale: 'es',
484
+ website_id: '42',
485
+ contents: [
486
+ { preset: 'getGlobalContentsByIDs', includeIds: [103433, 103420] },
487
+ { preset: 'getGlobalContentsByTags', tags: ['quizws'] },
488
+ ],
489
+ })
490
+ ```
491
+
492
+ ---
493
+
494
+ ## Features disponibles
495
+
496
+ | Key | Componente | Tags por defecto |
497
+ |-----|-----------|-----------------|
498
+ | `ringtone` | `Ringtone` | `ringtone` |
499
+ | `wallpapers` | `Wallpapers` | `wallpaper` |
500
+ | `wallpapersName` | `WallpapersName` | `wallpaper_name` |
501
+ | `memeGenerator` | `MemeGenerator` | `meme` |
502
+ | `quiz` | `Quiz` | `quiz` |
503
+ | `stickers` | `Stickers` | `stickers` |
504
+ | `avatarAI` | `AvatarAI` | (no aplica) |
505
+ | `creativeFaceSwap` | `CreativeFaceSwap` | (no aplica) |
506
+ | `arFilters` | `ARFilters` | (no aplica) |
507
+
508
+ ---
509
+
510
+ ## Uso de hooks fuera de ForYouPage
511
+
512
+ Los hooks también se exportan independientemente para casos donde se necesita lógica sin UI:
513
+
514
+ ```ts
515
+ import { useQuiz, useWallpapers, useRingtone, useStickers } from 'webs-sdk'
516
+
517
+ // Con multi-preset
518
+ const { quizItems, handleOpenQuiz } = useQuiz({
519
+ config: {
520
+ apiBaseUrl: 'https://api.winsports.co',
521
+ locale: 'es',
522
+ contents: [
523
+ { preset: 'getGlobalContentsByIDs', includeIds: [103433, 103420] },
524
+ { preset: 'getGlobalContentsByTags', tags: ['quizws'] },
525
+ ],
526
+ },
527
+ })
528
+ ```
529
+
530
+ ---
531
+
532
+ ## Ejemplo completo Winsports
533
+
534
+ ```tsx
535
+ import { ForYouPage } from 'webs-sdk'
536
+ import type { ForYouFeatures, ForYouLocale } from 'webs-sdk'
537
+
538
+ const WINSPORTS_FEATURES: ForYouFeatures = {
539
+ quiz: {
540
+ card: {
541
+ desktopImage: 'https://cdn.winsports.co/fanzone/quiz-desktop.png',
542
+ mobileImage: 'https://cdn.winsports.co/fanzone/quiz-mobile.png',
543
+ imageOnRight: true,
544
+ ctaColor: '#E6001E',
545
+ backgroundColor: '#F5F5F5',
546
+ },
547
+ config: {
548
+ apiBaseUrl: 'https://api.winsports.co',
549
+ locale: 'es',
550
+ website_id: '42',
551
+ contents: [
552
+ { preset: 'getGlobalContentsByIDs', includeIds: [103433, 103420, 103396, 103371] },
553
+ { preset: 'getGlobalContentsByTags', tags: ['quizws'] },
554
+ ],
555
+ },
556
+ onQuizStart: (quiz) => console.log('Quiz started:', quiz.title),
557
+ onQuizComplete: (score, total) => console.log(`Score: ${score}/${total}`),
558
+ },
559
+
560
+ ringtone: {
561
+ card: {
562
+ desktopImage: 'https://cdn.winsports.co/fanzone/ringtone-desktop.png',
563
+ imageOnRight: false,
564
+ ctaColor: '#E6001E',
565
+ backgroundColor: '#FFFFFF',
566
+ },
567
+ config: {
568
+ apiBaseUrl: 'https://api.winsports.co',
569
+ tags: ['ringtone', 'ringtone-winsports'],
570
+ locale: 'es',
571
+ website_id: '42',
572
+ },
573
+ },
574
+
575
+ wallpapers: {
576
+ card: {
577
+ desktopImage: 'https://cdn.winsports.co/fanzone/wallpapers-desktop.png',
578
+ ctaColor: '#E6001E',
579
+ backgroundColor: '#F5F5F5',
580
+ },
581
+ config: {
582
+ apiBaseUrl: 'https://api.winsports.co',
583
+ tags: 'wallpaper-winsports',
584
+ locale: 'es',
585
+ website_id: '42',
586
+ },
587
+ onDownloadSuccess: (w) => console.log('Downloaded:', w.title),
588
+ },
589
+
590
+ stickers: {
591
+ card: {
592
+ desktopImage: 'https://cdn.winsports.co/fanzone/stickers-desktop.png',
593
+ imageOnRight: false,
594
+ ctaColor: '#E6001E',
595
+ backgroundColor: '#FFFFFF',
596
+ },
597
+ config: {
598
+ apiBaseUrl: 'https://api.winsports.co',
599
+ tags: ['stickers', 'stickers-winsports'],
600
+ locale: 'es',
601
+ website_id: '42',
602
+ },
603
+ },
604
+ }
605
+
606
+ export default function FanzoneForYou() {
607
+ return (
608
+ <ForYouPage
609
+ locale="es"
610
+ features={WINSPORTS_FEATURES}
611
+ order={['quiz', 'ringtone', 'wallpapers', 'stickers']}
612
+ pageTheme={{
613
+ backgroundColor: '#FFFFFF',
614
+ sectionSpacing: '0',
615
+ backButtonIconColor: '#E6001E',
616
+ }}
617
+ />
618
+ )
619
+ }
620
+ ```
621
+
622
+ ---
623
+
624
+ ## Descargas y CORS — Proxy de medios
625
+
626
+ Algunos componentes del SDK (Stickers, MemeGenerator) descargan archivos desde CDNs de terceros.
627
+ Si el CDN no incluye el header `Access-Control-Allow-Origin`, el navegador bloquea la petición y
628
+ el SDK cae en el **fallback** abriendo la URL en una nueva pestaña en lugar de descargar el archivo.
629
+
630
+ > **Síntoma típico**: al pulsar "Download" en Stickers o "DOWNLOAD" en MemeGenerator, se abre el
631
+ > contenido en una nueva pestaña en lugar de descargarse directamente.
632
+
633
+ ### Solución: proxy de medios en el proyecto
634
+
635
+ Hay que crear una ruta API en el propio proyecto que haga de intermediario. Como la petición sale
636
+ del servidor (no del navegador), no hay restricciones CORS.
637
+
638
+ #### Next.js — App Router (`app/api/media-proxy/route.ts`)
639
+
640
+ ```ts
641
+ // app/api/media-proxy/route.ts
642
+ import { NextRequest, NextResponse } from 'next/server'
643
+
644
+ export async function GET(req: NextRequest) {
645
+ const url = req.nextUrl.searchParams.get('url')
646
+ if (!url) return new NextResponse('Missing url param', { status: 400 })
647
+
648
+ try {
649
+ const upstream = await fetch(url)
650
+ if (!upstream.ok) return new NextResponse('Upstream error', { status: upstream.status })
651
+
652
+ const blob = await upstream.blob()
653
+
654
+ return new NextResponse(blob, {
655
+ headers: {
656
+ 'Content-Type': upstream.headers.get('content-type') || 'application/octet-stream',
657
+ 'Content-Disposition': 'attachment',
658
+ 'Cache-Control': 'public, max-age=86400',
659
+ },
660
+ })
661
+ } catch {
662
+ return new NextResponse('Proxy error', { status: 500 })
663
+ }
664
+ }
665
+ ```
666
+
667
+ #### Next.js — Pages Router (`pages/api/media-proxy.ts`)
668
+
669
+ ```ts
670
+ // pages/api/media-proxy.ts
671
+ import type { NextApiRequest, NextApiResponse } from 'next'
672
+
673
+ export default async function handler(req: NextApiRequest, res: NextApiResponse) {
674
+ const url = req.query.url as string
675
+ if (!url) return res.status(400).send('Missing url param')
676
+
677
+ try {
678
+ const upstream = await fetch(url)
679
+ if (!upstream.ok) return res.status(upstream.status).send('Upstream error')
680
+
681
+ const buffer = Buffer.from(await upstream.arrayBuffer())
682
+ res.setHeader('Content-Type', upstream.headers.get('content-type') || 'application/octet-stream')
683
+ res.setHeader('Content-Disposition', 'attachment')
684
+ res.setHeader('Cache-Control', 'public, max-age=86400')
685
+ res.send(buffer)
686
+ } catch {
687
+ res.status(500).send('Proxy error')
688
+ }
689
+ }
690
+ ```
691
+
692
+ ### Configurar el proxy en ForYouPage
693
+
694
+ Una vez creada la ruta, pásala al `ForYouPage` mediante las props correspondientes:
695
+
696
+ ```tsx
697
+ <ForYouPage
698
+ locale="es"
699
+ features={{
700
+ stickers: {
701
+ config: { ... },
702
+ // ← ruta relativa del proxy: sin CORS porque es el mismo origen
703
+ downloadProxyUrl: '/api/media-proxy?url=',
704
+ },
705
+ memeGenerator: {
706
+ config: { ... },
707
+ // ← mismo proxy, el SDK lo usa para cargar la imagen en el canvas
708
+ imageProxyUrl: '/api/media-proxy?url=',
709
+ },
710
+ }}
711
+ />
712
+ ```
713
+
714
+ ### Configurar el proxy en componentes standalone
715
+
716
+ ```tsx
717
+ // Stickers standalone
718
+ <Stickers
719
+ config={stickersConfig}
720
+ downloadProxyUrl="/api/media-proxy?url="
721
+ />
722
+
723
+ // MemeGenerator standalone
724
+ <MemeGenerator
725
+ config={memeConfig}
726
+ imageProxyUrl="/api/media-proxy?url="
727
+ />
728
+ ```
729
+
730
+ ### Usar `downloadFile` del SDK directamente
731
+
732
+ Si necesitas descargar archivos en tu propio código (fuera de los componentes), el SDK exporta
733
+ la función `downloadFile`:
734
+
735
+ ```ts
736
+ import { downloadFile } from 'webs-sdk'
737
+
738
+ // Con proxy (recomendado para CDNs de terceros):
739
+ await downloadFile(
740
+ 'https://cdn.example.com/sticker.png',
741
+ 'mi-sticker.png',
742
+ '/api/media-proxy?url='
743
+ )
744
+
745
+ // Sin proxy (solo si el CDN tiene CORS habilitado):
746
+ await downloadFile('https://cdn.example.com/sticker.png', 'mi-sticker.png')
747
+ ```
748
+
749
+ > `downloadFile` intenta descargar via fetch+blob. Si falla (p.ej. CORS sin proxy), abre
750
+ > la URL en una nueva pestaña como fallback.
751
+
752
+ ---
753
+
754
+ ## Feature: Quiz — tipos de quiz y pantalla de resultado
755
+
756
+ El componente `Quiz` diferencia automáticamente entre **3 modos** según los datos del backend:
757
+
758
+ ### Modo 1: Quiz por puntos (`personality = false`)
759
+
760
+ Cada respuesta tiene un valor numérico (`puntos`). Se muestra la puntuación obtenida vs el máximo posible, y el mensaje de resultado correspondiente al rango.
761
+
762
+ ```
763
+ 🏆
764
+ "You got 30/80 points" ← score / dynamicMax (suma de maxPoints por pregunta)
765
+ "Vous avez des notions..." ← resultMessage del rango matched
766
+ ```
767
+
768
+ ### Modo 2: Quiz de personalidad (`personality = true`)
769
+
770
+ El campo `personality: true` en el `metadata` del item indica que **no se debe mostrar la puntuación numérica**. Solo aparece el mensaje de resultado en grande.
771
+
772
+ ```
773
+ 🏆
774
+ "Eres un experto del fútbol" ← solo resultMessage, sin puntuación
775
+ ```
776
+
777
+ ### Modo 3: Quiz legacy (correcto/incorrecto)
778
+
779
+ Items sin `quiz_configuration` (formato antiguo con `correct_option` / `option_a..d`). Se muestra el número de aciertos sobre el total de preguntas.
780
+
781
+ ```
782
+ 🏆
783
+ "3/5" ← score / totalPreguntas
784
+ ```
785
+
786
+ ### Cómo el SDK detecta el modo
787
+
788
+ El `personality` se extrae automáticamente del `metadata` del item de la lista:
789
+
790
+ ```json
791
+ {
792
+ "id": 100,
793
+ "title": "¿Qué entrenador eres?",
794
+ "contents": "832",
795
+ "metadata": "{\"personality\": true}"
796
+ }
797
+ ```
798
+
799
+ El SDK acepta tanto `boolean` como `string 'true'`. **No hay que configurar nada en el proyecto** — el SDK detecta el tipo por sí solo.
800
+
801
+ ### Cálculo del total máximo (`dynamicMax`)
802
+
803
+ El SDK calcula el total máximo real sumando el `maxPoints` de cada pregunta (la opción con más puntos de cada pregunta):
804
+
805
+ ```
806
+ dynamicMax = Σ Math.max(...options[i].points)
807
+ ```
808
+
809
+ Fallbacks si alguna pregunta no tiene `maxPoints`:
810
+ 1. `results[last].maxPoints` (tope del último resultado definido en el backend)
811
+ 2. `score` del usuario (fallback extremo)
812
+
813
+ ### `scoreLabel` — personalización
814
+
815
+ El texto del score es configurable vía `texts.scoreLabel`. Usa `{score}` y `{total}` como variables:
816
+
817
+ ```tsx
818
+ quiz: {
819
+ texts: {
820
+ scoreLabel: 'Obtuviste {score} de {total} puntos', // es
821
+ // o
822
+ scoreLabel: 'Tu as obtenu {score}/{total} points', // fr
823
+ }
824
+ }
825
+ ```
826
+
827
+ Default del SDK: `'You got {score}/{total} points'`
828
+
829
+ ---
830
+
831
+ ## Feature: WallpapersName — modo dual (clásico vs AETemplates)
832
+
833
+ La feature `wallpapersName` soporta **dos modos** seleccionados automáticamente según las props:
834
+
835
+ ### Modo clásico (sistema anterior)
836
+
837
+ Usa `WallpapersName` con contenido cargado vía `get_contents_by_tags`. Compatible con el sistema antiguo.
838
+
839
+ ```tsx
840
+ wallpapersName: {
841
+ card: { desktopImage: '/img/wallpapers-name.jpg' },
842
+ config: {
843
+ apiUrl: '/api/get_contents_by_tags',
844
+ tags: 'wallpaper_name',
845
+ website_id: '42',
846
+ locale: 'es',
847
+ },
848
+ howItWorksSteps: [
849
+ { title: 'Elige una plantilla', description: 'Busca la que más te guste' },
850
+ { title: 'Escribe tu nombre', description: 'Personaliza con tu nombre' },
851
+ { title: 'Descarga', description: 'Guárdalo en tu móvil' },
852
+ ],
853
+ }
854
+ ```
855
+
856
+ ### Modo nuevo — AETemplates (sistema unificado)
857
+
858
+ Cuando se proporciona `aeTemplatesConfig`, `ForYouPage` renderiza `WallpapersNamePage`, que integra en una sola pieza:
859
+ 1. **Header** con título y descripción
860
+ 2. **"How does it work?"** — carousel de pasos con dots de navegación
861
+ 3. **AETemplatesSection** — motor de templates de After Effects con tabs, modal de personalización y botón CUSTOMISE
862
+
863
+ ```tsx
864
+ import type { WallpapersNameFeatureOverride } from 'webs-sdk'
865
+
866
+ const wallpapersNameFeature: WallpapersNameFeatureOverride = {
867
+ card: {
868
+ desktopImage: '/img/wallpapers-name-desktop.jpg',
869
+ ctaColor: '#E6001E',
870
+ },
871
+ // Textos del header
872
+ pageTexts: {
873
+ title: 'Wallpapers con tu nombre',
874
+ description: 'Personaliza wallpapers deportivos con tu nombre o el de tu equipo',
875
+ howItWorksTitle: '¿Cómo funciona?',
876
+ },
877
+ // Pasos del carousel "Cómo funciona"
878
+ howItWorksSteps: [
879
+ { title: 'Elige una plantilla', image: '/img/step1.jpg' },
880
+ { title: 'Escribe tu nombre', image: '/img/step2.jpg' },
881
+ { title: 'Descarga', image: '/img/step3.jpg' },
882
+ ],
883
+ // ← La presencia de aeTemplatesConfig activa el modo nuevo
884
+ aeTemplatesConfig: {
885
+ apiUrl: '/api/get_ae_templates',
886
+ langId: 27,
887
+ clientId: 50,
888
+ tabs: [
889
+ { id: 'entertainment', label: 'Entertainment', type: 9 },
890
+ { id: 'sports', label: 'Sports', type: 10 },
891
+ ],
892
+ customiseApiUrl: '/api/create-custom-content',
893
+ userId: user.id,
894
+ wid: website_id,
895
+ webLang: locale,
896
+ },
897
+ aeTemplatesTexts: {
898
+ customiseButton: 'PERSONALIZAR',
899
+ downloadButton: 'Descargar',
900
+ loadingText: 'Cargando plantillas...',
901
+ noTemplatesText: 'No hay plantillas disponibles',
902
+ },
903
+ aeTemplatesTheme: {
904
+ primaryColor: '#E6001E',
905
+ buttonTextColor: '#FFFFFF',
906
+ },
907
+ }
908
+ ```
909
+
910
+ ### Props de `WallpapersNameFeatureOverride`
911
+
912
+ | Prop | Tipo | Descripción |
913
+ |------|------|-------------|
914
+ | `card` | `ForYouCardVisual` | Visual de la card en el hub. |
915
+ | `config` | `ForYouFeatureConfig & { wid? }` | Config para el modo clásico. |
916
+ | `howItWorksSteps` | `WallpapersNameHowItWorksStep[]` | Pasos del carousel "¿Cómo funciona?". |
917
+ | `aeTemplatesConfig` | `AETemplatesConfig` | **Activa el modo nuevo.** Config de AETemplates. |
918
+ | `aeTemplatesTexts` | `AETemplatesTexts` | Textos de la sección AETemplates. |
919
+ | `aeTemplatesTheme` | `AETemplatesTheme` | Tema de colores de AETemplates. |
920
+ | `pageTexts` | `WallpapersNamePageTexts` | Textos del header (título, descripción, howItWorksTitle). Solo modo nuevo. |
921
+ | `pageTheme` | `WallpapersNamePageTheme` | Tema del header. Solo modo nuevo. |
922
+
923
+ > **Regla de selección:** si `aeTemplatesConfig` está definido → `WallpapersNamePage` (modo nuevo); si no → `WallpapersName` clásico.
924
+
925
+ ---
926
+
927
+ ## Feature: AETemplatesSection — fuente dual (get_ae_templates + get_contents_by_tags)
928
+
929
+ A partir de `v0.18.82`, `AETemplatesSection` puede combinar **dos fuentes de datos** en el mismo selector de tabs.
930
+
931
+ ### Cuándo usarlo
932
+
933
+ Cuando tienes templates de After Effects en `get_ae_templates` **y** además contenidos clásicos en `get_contents_by_tags` que quieres mostrar como templates personalizables en el mismo interfaz.
934
+
935
+ ### Props nuevas en `AETemplatesConfig`
936
+
937
+ | Prop | Tipo | Default | Descripción |
938
+ |------|------|---------|-------------|
939
+ | `enableDefaultTemplates` | `boolean` | `true` | Activa los tabs de `get_ae_templates` (`apiUrl`). |
940
+ | `enableCustomTemplates` | `boolean` | `false` | Activa los tabs de `get_contents_by_tags` (`customApiUrl`). |
941
+ | `customApiUrl` | `string` | - | URL del endpoint `get_contents_by_tags`. |
942
+ | `customApiTags` | `string[]` | - | Tags para la llamada a `customApiUrl`. |
943
+ | `customTabs` | `AETab[]` | - | Tabs adicionales que llaman a `customApiUrl`. Se añaden después de los tabs default en el selector. |
944
+
945
+ ### Campo `custom_content` en cada item
946
+
947
+ El botón **Customise** de cada card se habilita o deshabilita según el campo `custom_content` del item:
948
+
949
+ | Valor del campo | Estado del botón |
950
+ |-----------------|-----------------|
951
+ | `"1"` o `1` | ✅ Activo — abre el modal de personalización |
952
+ | Ausente, `null`, otro valor | 🚫 Deshabilitado (gris, `cursor: not-allowed`) |
953
+
954
+ Para los items de `get_ae_templates`, el campo viene directamente del endpoint.
955
+ Para los items de `get_contents_by_tags`, el SDK lo lee del `metadata` del item:
956
+
957
+ ```json
958
+ {
959
+ "id": 123,
960
+ "title": "Mi plantilla",
961
+ "thumbnail": "https://cdn.example.com/preview.jpg",
962
+ "metadata": "{\"custom_content\": \"1\"}"
963
+ }
964
+ ```
965
+
966
+ ### Ejemplo: solo fuente AETemplates (comportamiento anterior)
967
+
968
+ ```tsx
969
+ aeTemplatesConfig: {
970
+ apiUrl: '/api/get_ae_templates',
971
+ langId: 27,
972
+ clientId: 50,
973
+ tabs: [
974
+ { id: 'sports', label: 'Sports', type: 10 },
975
+ { id: 'entertainment', label: 'Entertainment', type: 9 },
976
+ ],
977
+ // enableDefaultTemplates: true ← por defecto, no hace falta declararlo
978
+ customiseApiUrl: '/api/create-custom-content',
979
+ userId: user.id,
980
+ wid: '42',
981
+ }
982
+ ```
983
+
984
+ ### Ejemplo: solo fuente custom (get_contents_by_tags)
985
+
986
+ ```tsx
987
+ aeTemplatesConfig: {
988
+ apiUrl: '/api/get_ae_templates', // requerido aunque no se use
989
+ langId: 27,
990
+ clientId: 50,
991
+ tabs: [], // sin tabs default
992
+ enableDefaultTemplates: false, // ← desactiva get_ae_templates
993
+ enableCustomTemplates: true, // ← activa get_contents_by_tags
994
+ customApiUrl: '/api/get_contents_by_tags',
995
+ customApiTags: ['wallpaper_name'],
996
+ customTabs: [
997
+ { id: 'mis-plantillas', label: 'Mis Plantillas', type: 0 },
998
+ ],
999
+ customiseApiUrl: '/api/create-custom-content',
1000
+ userId: user.id,
1001
+ wid: '42',
1002
+ }
1003
+ ```
1004
+
1005
+ ### Ejemplo: ambas fuentes activas a la vez
1006
+
1007
+ Los tabs de ambas fuentes aparecen en el mismo selector. Al cambiar de tab, el SDK detecta automáticamente cuál endpoint llamar.
1008
+
1009
+ ```tsx
1010
+ aeTemplatesConfig: {
1011
+ // ── Fuente 1: get_ae_templates ────────────────────────────────────────
1012
+ apiUrl: '/api/get_ae_templates',
1013
+ langId: 27,
1014
+ clientId: 50,
1015
+ enableDefaultTemplates: true,
1016
+ tabs: [
1017
+ { id: 'sports', label: 'Sports', type: 10 },
1018
+ { id: 'entertainment', label: 'Entertainment', type: 9 },
1019
+ { id: 'abstract', label: 'Abstract', type: 11 },
1020
+ ],
1021
+
1022
+ // ── Fuente 2: get_contents_by_tags ───────────────────────────────────
1023
+ enableCustomTemplates: true,
1024
+ customApiUrl: '/api/get_contents_by_tags',
1025
+ customApiTags: ['wallpaper_name'],
1026
+ customTabs: [
1027
+ { id: 'mis-plantillas', label: 'Mis Plantillas', type: 0 },
1028
+ ],
1029
+
1030
+ // ── Config compartida ─────────────────────────────────────────────────
1031
+ customiseApiUrl: '/api/create-custom-content',
1032
+ userId: user.id,
1033
+ wid: '42',
1034
+ webLang: 'es',
1035
+ }
1036
+ ```
1037
+
1038
+ El selector de tabs resultante sería:
1039
+
1040
+ ```
1041
+ [Sports] [Entertainment] [Abstract] [Mis Plantillas]
1042
+ ↑ get_ae_templates ↑ get_contents_by_tags
1043
+ ```
1044
+
1045
+ ### Deduplicación automática
1046
+
1047
+ Si un mismo `id` aparece en ambas fuentes (caso raro), el SDK **descarta el duplicado** manteniendo el primero que llegó.
1048
+
1049
+ ### Comportamiento del scrollbar en el selector de tabs
1050
+
1051
+ El selector de tabs tiene scroll horizontal en caso de overflow (muchas tabs en móvil), pero el scrollbar nativo está **oculto visualmente** para todos los navegadores (`scrollbar-width: none` + `::-webkit-scrollbar { display: none }`). El scroll sigue siendo funcional.
1052
+
1053
+ ---
1054
+
1055
+ ## Feature: CreativeFaceSwap — ejemplos + endpoints
1056
+
1057
+ El componente `CreativeFaceSwap` permite a los usuarios subir una foto, elegir un ejemplo de carrusel y generar una imagen nueva con IA (face swap / creative generation).
1058
+
1059
+ ### Partes del componente
1060
+
1061
+ ```
1062
+ ┌─────────────────────────┬──────────────────────────────────┐
1063
+ │ Columna izquierda │ Columna derecha │
1064
+ │ ───────────────── │ ────────────── │
1065
+ │ · Upload foto │ · Textarea descripción │
1066
+ │ · Carrusel ejemplos │ · Botón CREATE │
1067
+ │ │ · Área resultado (progreso/img) │
1068
+ └─────────────────────────┴──────────────────────────────────┘
1069
+ ```
1070
+
1071
+ ### Props de `CreativeFaceSwapFeatureOverride`
1072
+
1073
+ | Prop | Tipo | Descripción |
1074
+ |------|------|-------------|
1075
+ | `card` | `ForYouCardVisual` | Visual de la card en el hub. |
1076
+ | `config` | `FaceSwapConfig` | Endpoints de generación + credenciales. **Requerido.** |
1077
+ | `examples` | `FaceSwapExample[]` | Ejemplos estáticos para el carrusel (prioridad máxima). |
1078
+ | `examplesTag` | `string` | Tag para cargar ejemplos vía `get_contents_by_tags` (carga interna del componente). |
1079
+ | `contentsConfig` | `ForYouFeatureConfig` | **Nuevo en v0.18.83.** Config de fetch de ejemplos con presets (multi-source). Prioridad sobre `examplesTag`. |
1080
+ | `theme` | `Partial<FaceSwapTheme>` | Override del tema de colores. |
1081
+ | `texts` | `Partial<FaceSwapTexts>` | Override de textos. |
1082
+ | `apiBaseUrl` | `string` | Base URL para el fetch de `examplesTag` (si se usa). |
1083
+ | `locale` | `string` | Locale override para esta feature. |
1084
+ | `onGenerationComplete` | `(imageUrl: string) => void` | Callback cuando la imagen está lista. |
1085
+ | `onError` | `(error: string) => void` | Callback de error. |
1086
+
1087
+ ### `config` — endpoints de generación (FaceSwapConfig)
1088
+
1089
+ Este objeto **no** carga los ejemplos del carrusel. Contiene los endpoints del servicio de generación de imágenes y las credenciales del usuario:
1090
+
1091
+ ```ts
1092
+ config: {
1093
+ userId: USER_ID, // number — ID del usuario
1094
+ userLogin: USER_LOGIN, // number — login del usuario
1095
+ wid: WEBSITE_ID, // string — website ID (opcional)
1096
+ apiEndpoints: {
1097
+ generate: '/api/face-swap/generate', // POST — inicia el job de generación
1098
+ progress: '/api/face-swap/progress', // POST — polling del % de progreso
1099
+ getImage: '/api/face-swap/get-image', // POST — obtiene la imagen final
1100
+ },
1101
+ }
1102
+ ```
1103
+
1104
+ > ⚠️ Estas rutas deben existir como **rutas API en el proyecto** (no son endpoints del CMS).
1105
+ > El SDK llama a estas rutas directamente desde el navegador del usuario.
1106
+
1107
+ ### `contentsConfig` — carga de ejemplos del carrusel (v0.18.83+)
1108
+
1109
+ Permite cargar los ejemplos del carrusel desde la API del CMS usando el sistema de presets:
1110
+
1111
+ ```tsx
1112
+ creativeFaceSwap: {
1113
+ config: {
1114
+ userId: USER_ID,
1115
+ userLogin: USER_LOGIN,
1116
+ wid: WEBSITE_ID,
1117
+ apiEndpoints: {
1118
+ generate: '/api/face-swap/generate',
1119
+ progress: '/api/face-swap/progress',
1120
+ getImage: '/api/face-swap/get-image',
1121
+ },
1122
+ },
1123
+ // ← carga los ejemplos del carrusel desde el CMS
1124
+ contentsConfig: {
1125
+ apiBaseUrl: 'https://api.example.com',
1126
+ locale: 'en',
1127
+ website_id: WEBSITE_ID,
1128
+ contents: [
1129
+ {
1130
+ preset: 'getGlobalContentsByIDs',
1131
+ includeIds: [104557, 100180, 98423, 97811],
1132
+ },
1133
+ ],
1134
+ },
1135
+ }
1136
+ ```
1137
+
1138
+ También puedes combinar con tags:
1139
+
1140
+ ```ts
1141
+ contentsConfig: {
1142
+ apiBaseUrl: 'https://api.example.com',
1143
+ locale: 'en',
1144
+ contents: [
1145
+ { preset: 'getGlobalContentsByIDs', includeIds: [104557, 100180] },
1146
+ { preset: 'getGlobalContentsByTags', tags: ['faceswap-examples'] },
1147
+ ],
1148
+ }
1149
+ ```
1150
+
1151
+ O solo con un tag simple:
1152
+
1153
+ ```ts
1154
+ contentsConfig: {
1155
+ apiBaseUrl: 'https://api.example.com',
1156
+ locale: 'en',
1157
+ tags: 'faceswap-examples',
1158
+ }
1159
+ ```
1160
+
1161
+ ### Prioridad de ejemplos
1162
+
1163
+ El SDK aplica este orden (el primero que tenga datos gana):
1164
+
1165
+ ```
1166
+ 1. examples[] → array estático pasado en la prop
1167
+ 2. contentsConfig → fetch interno con forYouFetch (v0.18.83+)
1168
+ 3. examplesTag → fetch simple interno del componente
1169
+ ```
1170
+
1171
+ ### Qué campos necesita cada item del CMS
1172
+
1173
+ | Campo del item | Se usa como |
1174
+ |----------------|-------------|
1175
+ | `thumbnail` | Imagen del ejemplo en el carrusel |
1176
+ | `images.img1` | Imagen (fallback si no hay thumbnail) |
1177
+ | `imageUrl` | Imagen (segundo fallback) |
1178
+ | `title` | Descripción debajo de la imagen |
1179
+ | `description` | Descripción (fallback si no hay title) |
1180
+
1181
+ > Si el item no tiene ningún campo de imagen, **se descarta** y no aparece en el carrusel.
1182
+
1183
+ ### Personalización: `FaceSwapTheme`
1184
+
1185
+ | Prop | Default | Descripción |
1186
+ |------|---------|-------------|
1187
+ | `primaryColor` | `'#FF5500'` | Color principal (botones, spinner). |
1188
+ | `primaryHoverColor` | `'#ff1600'` | Color hover de botones. |
1189
+ | `secondaryColor` | `'#001B5E'` | Color secundario. |
1190
+ | `textColor` | `'#001B5E'` | Color del texto. |
1191
+ | `backgroundColor` | `'#F9F9F9'` | Fondo del componente. |
1192
+
1193
+ ### Personalización: `FaceSwapTexts`
1194
+
1195
+ | Prop | Default |
1196
+ |------|---------|
1197
+ | `uploadTitle` | `'Your original photo'` |
1198
+ | `uploadButton` | `'UPLOAD IMAGE'` |
1199
+ | `changeImageButton` | `'CHANGE IMAGE'` |
1200
+ | `examplesTitle` | `'Get inspired by these examples'` |
1201
+ | `tryButton` | `'TRY'` |
1202
+ | `describeTitle` | `'Describe your desired image'` |
1203
+ | `descriptionPlaceholder` | `'Young curly-haired girl...'` |
1204
+ | `createButton` | `'CREATE'` |
1205
+ | `generatingButton` | `'GENERATING...'` |
1206
+ | `resultTitle` | `'Your result'` |
1207
+ | `emptyStateText` | `'Your generated image will appear here'` |
1208
+ | `startAgainButton` | `'START AGAIN'` |
1209
+ | `downloadButton` | `'DOWNLOAD'` |
1210
+ | `progressText` | `'Progress'` |
1211
+ | `successMessage` | `'Image generated successfully!'` |
1212
+
1213
+ ### Ejemplo completo
1214
+
1215
+ ```tsx
1216
+ import { ForYouPage } from 'webs-sdk'
1217
+ import type { ForYouFeatures } from 'webs-sdk'
1218
+
1219
+ const features: ForYouFeatures = {
1220
+ creativeFaceSwap: {
1221
+ card: {
1222
+ desktopImage: '/images/faceswap-desktop.jpg',
1223
+ mobileImage: '/images/faceswap-mobile.jpg',
1224
+ imageOnRight: true,
1225
+ ctaColor: '#FF5500',
1226
+ backgroundColor: '#F9F9F9',
1227
+ },
1228
+ config: {
1229
+ userId: USER_ID,
1230
+ userLogin: USER_LOGIN,
1231
+ wid: WEBSITE_ID,
1232
+ apiEndpoints: {
1233
+ generate: '/api/face-swap/generate',
1234
+ progress: '/api/face-swap/progress',
1235
+ getImage: '/api/face-swap/get-image',
1236
+ },
1237
+ },
1238
+ contentsConfig: {
1239
+ apiBaseUrl: 'https://api.example.com',
1240
+ locale: 'en',
1241
+ website_id: WEBSITE_ID,
1242
+ contents: [
1243
+ { preset: 'getGlobalContentsByIDs', includeIds: [104557, 100180, 98423] },
1244
+ ],
1245
+ },
1246
+ theme: {
1247
+ primaryColor: '#FF5500',
1248
+ textColor: '#001B5E',
1249
+ },
1250
+ texts: {
1251
+ uploadTitle: 'Tu foto',
1252
+ createButton: 'GENERAR',
1253
+ examplesTitle: 'Inspírate en estos ejemplos',
1254
+ },
1255
+ onGenerationComplete: (imageUrl) => console.log('Listo:', imageUrl),
1256
+ onError: (error) => console.error('Error:', error),
1257
+ },
1258
+ }
1259
+
1260
+ export default function ForYouSection() {
1261
+ return <ForYouPage locale="en" features={features} />
1262
+ }
1263
+ ```
1264
+
1265
+ ### Respuesta esperada de los endpoints
1266
+
1267
+ El SDK espera las siguientes estructuras JSON. Si el servicio tiene un formato diferente, hay que crear una ruta proxy en el proyecto que adapte la respuesta.
1268
+
1269
+ **`/generate` → POST**
1270
+ ```json
1271
+ { "success": true, "data": { "md5": "abc123", "i_jobs": 1 } }
1272
+ ```
1273
+
1274
+ **`/progress` → POST**
1275
+ ```json
1276
+ { "success": true, "data": { "pct": 45 } }
1277
+ ```
1278
+
1279
+ **`/get-image` → POST**
1280
+ ```json
1281
+ { "success": true, "data": { "s3_image": "https%3A%2F%2Fcdn.example.com%2Fresult.jpg" } }
1282
+ ```
1283
+
1284
+ > `s3_image` debe estar URL-encoded. El SDK aplica `decodeURIComponent()` internamente.
1285
+
1286
+ ### Fix en v0.18.83 — error HTML en el endpoint generate
1287
+
1288
+ Si el endpoint `/generate` no existe o falla con un 404/500, Next.js devuelve una página HTML en lugar de JSON. Antes de v0.18.83, esto causaba un crash poco descriptivo:
1289
+
1290
+ ```
1291
+ SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
1292
+ ```
1293
+
1294
+ A partir de v0.18.83, el SDK detecta el `Content-Type` de la respuesta antes de parsear. Si no es JSON, muestra un error legible en la UI:
1295
+
1296
+ ```
1297
+ HTTP 404: The generate endpoint returned an unexpected response.
1298
+ Check that the API route exists and is correctly configured in this project.
1299
+ ```
1300
+
1301
+ ---
1302
+
1303
+ ## Migración desde el sistema anterior
1304
+
1305
+ Si tienes el sistema antiguo funcionando, **no necesitas migrar todavía**. Ambos sistemas coexisten.
1306
+
1307
+ Cuando quieras migrar, el patrón es:
1308
+
1309
+ ```tsx
1310
+ // ANTES
1311
+ import { Quiz } from 'webs-sdk'
1312
+ <Quiz config={{ apiUrl: '/api/get_contents_by_tags', tags: ['quiz'], locale: 'es' }} />
1313
+
1314
+ // DESPUÉS (dentro de ForYouPage)
1315
+ <ForYouPage
1316
+ locale="es"
1317
+ features={{
1318
+ quiz: {
1319
+ card: { desktopImage: '/img/quiz.jpg' }, // ← para mostrar la card en el hub
1320
+ config: { tags: 'quiz' }, // ← misma config de antes
1321
+ }
1322
+ }}
1323
+ />
1324
+ ```
1325
+
1326
+ Los componentes individuales (`Quiz`, `Wallpapers`, `Ringtone`, etc.) **siguen funcionando exactamente igual** que antes.
1327
+
1328
+ ---
1329
+
1330
+ ## Changelog
1331
+
1332
+ ### 0.18.83
1333
+ - `CreativeFaceSwapFeatureOverride`: nueva prop `contentsConfig?: ForYouFeatureConfig` — permite cargar los ejemplos del carrusel desde el CMS usando el sistema de presets (`getGlobalContentsByIDs`, `getGlobalContentsByTags`, etc.), igual que el resto de features.
1334
+ - `ForYouPage` case `creativeFaceSwap`: nuevo sub-componente interno `CreativeFaceSwapWithContents` que ejecuta `forYouFetch(contentsConfig)` al montar, mapea los items a `FaceSwapExample[]` y los pasa como `examples` al componente.
1335
+ - Prioridad de ejemplos: `examples[]` estáticos → `contentsConfig` (fetch interno) → `examplesTag` (fetch del componente).
1336
+ - `CreativeFaceSwap.tsx` — `handleCreate`: detección de respuesta HTML antes de `response.json()`. Si el endpoint devuelve un 404/500 de Next.js (HTML en lugar de JSON), el SDK lanza un error legible (`"HTTP 404: The generate endpoint returned an unexpected response..."`) en lugar de crashear con `SyntaxError`.
1337
+
1338
+ ### 0.18.82
1339
+ - `AETemplatesConfig`: nuevas props `enableDefaultTemplates`, `enableCustomTemplates`, `customApiUrl`, `customApiTags`, `customTabs` — soporte para fuente dual (`get_ae_templates` + `get_contents_by_tags`) en el mismo selector de tabs.
1340
+ - `AETemplateItem`: nuevo campo `custom_content?: string | number` — el botón Customise se activa solo si el item devuelve `"1"` o `1`. En caso contrario aparece deshabilitado.
1341
+ - `AETemplatesSection`: selector de tabs dinámico (fusiona tabs default + customTabs), fetch por fuente, `Promise.all` + deduplicación por `id`, fallback automático al primer tab si el activo desaparece.
1342
+ - `mapContentToAETemplateItem`: helper interno que normaliza items de `get_contents_by_tags` → `AETemplateItem`, leyendo `custom_content` del `metadata` o del campo raíz.
1343
+ - Scrollbar del selector de tabs oculto visualmente (`scrollbar-width: none` + `::-webkit-scrollbar { display: none }`).
1344
+
1345
+ ### 0.18.80
1346
+ - `WallpapersNamePage` (carousel "How does it work?"): eliminados `backgroundColor` (#EAECF0) y `padding` del card de paso — sin fondo, sin contenedor visible.
1347
+ - `WallpapersNamePage`: `maxWidth` de la imagen del paso `260px` → `600px` + `width: 100%` — imagen más grande y responsiva.
1348
+
1349
+ ### 0.18.79
1350
+ - `WallpapersNamePage` (carousel "How does it work?"): fondo del card de paso `#FFFDE7` (amarillo) → `#EAECF0` (gris neutro).
1351
+ - `WallpapersNamePage`: `maxWidth` de imagen `160px` → `260px`.
1352
+ - `WallpapersNamePage`: padding del card `40px 24px` → `48px 32px 36px`.
1353
+ - `WallpapersNamePage`: tipografía del título del paso `1.1rem` → `1.25rem`.
1354
+ - `WallpapersNamePage`: tipografía de descripción `0.9rem` → `0.95rem` + `maxWidth: 320px`.
1355
+
1356
+ ### 0.18.78
1357
+ - `Quiz`: fix `scoreLabel` — el default `'You got {score}/{total} {type}'` tenía `{type}` que nunca se reemplazaba, apareciendo literalmente en pantalla. Corregido a `'You got {score}/{total} points'`.
1358
+ - `Quiz`: eliminado carácter `←` hardcodeado en el botón "Previous". El texto es ahora controlado exclusivamente por `texts.previousButton`.
1359
+
1360
+ ### 0.18.77
1361
+ - `Quiz`: nuevo campo `QuizItem.personality?: boolean` — extraído del `metadata` del item. Acepta `boolean` o `string 'true'`. Identifica quizzes de personalidad.
1362
+ - `Quiz`: nuevo campo `QuizQuestion.maxPoints?: number` — máximo de puntos alcanzable por pregunta (`Math.max(...options.map(o => o.points))`).
1363
+ - `Quiz`: cálculo de `totalMax` (dynamicMax) corregido — usa `Σ q.maxPoints` para el total real en lugar de un valor fijo del backend.
1364
+ - `Quiz`: pantalla de resultado diferenciada en 3 modos: personality (solo mensaje), puntos (score/max + mensaje) y legacy (aciertos/total).
1365
+ - `Quiz`: `personality` propagado desde `activeQuiz` al `<QuizModal>`.
1366
+ - `Quiz`: optional chaining en `options[optionIdx]?.points ?? 0` para evitar crashes con índices inválidos.
1367
+
1368
+ ### 0.18.76
1369
+ - `WallpapersName`: nuevo componente `WallpapersNamePage` — integra header, carousel "¿Cómo funciona?" y `AETemplatesSection` en una sola pieza.
1370
+ - `ForYouPage` case `wallpapersName`: modo dual — si `aeTemplatesConfig` está presente renderiza `WallpapersNamePage` (modo nuevo); si no, usa `WallpapersName` clásico.
1371
+ - Nuevas props en `WallpapersNameFeatureOverride`: `aeTemplatesConfig`, `aeTemplatesTexts`, `aeTemplatesTheme`, `pageTexts`, `pageTheme`.
1372
+ - Nuevos tipos exportados: `WallpapersNamePageTexts`, `WallpapersNamePageTheme`, `WallpapersNamePageProps`.
1373
+
1374
+ ### 0.18.75
1375
+ - `ForYouPage` case `wallpapersName`: normalización `website_id` → `wid` (campo específico de `WallpapersNameConfig`). Fix para "No wallpapers available" al usar `ForYouPage` con la feature `wallpapersName`.
1376
+ - Auditoría completa de normalizaciones: el resto de cases (`ringtone`, `wallpapers`, `memeGenerator`, `quiz`, `stickers`, `arFilters`) ya normalizaban correctamente.
1377
+
1378
+ ### 0.18.74
1379
+ - Nuevo util exportado: `downloadFile` — función de descarga con soporte de proxy CORS. Extrae la lógica inline de `Stickers.tsx` a `src/utils/downloadFile.ts` y la exporta desde `webs-sdk`.
1380
+ - `Stickers.tsx`: usa `downloadFile` del util en lugar de definición inline.
1381
+ - Documentación: nueva sección **"Descargas y CORS — Proxy de medios"** en la guía de implementación con código del endpoint proxy (Next.js App Router y Pages Router) y configuración de `downloadProxyUrl` / `imageProxyUrl`.
1382
+
1383
+ ### 0.18.72
1384
+ - `ForYouPage`: scroll al top usa `window.scrollTo({ top: 0 })` en lugar de `scrollIntoView` (scroll ahora va al top de la ventana, no solo al componente)
1385
+ - `ForYouPage` case `ringtone`: normaliza correctamente `tags` → `ringtoneTag` (string) que espera `Ringtone.tsx`
1386
+ - `ForYouPage` case `quiz`: `apiUrl` siempre tiene valor (construido con `buildPresetUrl` si se usa `apiBaseUrl`); se pasa `questionsApiUrl` correctamente a `Quiz.tsx`
1387
+ - `ForYouPage` case `memeGenerator`: `apiUrl` siempre tiene valor; normaliza `tags` → `memeTag` (string) que espera `MemeGenerator.tsx`
1388
+ - `ForYouPage` case `wallpapers`: nuevo prop `tabs?: WallpapersTab[]` — selector de pestañas por categoría encima del componente Wallpapers
1389
+ - `ForYouPage` case `wallpapers`: nuevo prop `tabsTheme` — personalización visual del selector de tabs
1390
+ - `ForYouPage` case `wallpapers`: `heroBannerImage` y `fallbackImage` se pasan correctamente al config de `Wallpapers.tsx`
1391
+ - Nuevo tipo exportado: `WallpapersTab`
1392
+
1393
+ ### 0.18.70
1394
+ - `ForYouPage`: nuevo prop `hubBanner` (`ForYouHubBanner`) — banner hero opcional encima de las cards en el hub
1395
+ - `ForYouHubBanner`: soporta imagen desktop + mobile, título, subtítulo, color de fondo, altura y padding configurables
1396
+ - `ForYouHubBannerHighlight`: barra de highlights con icono (SVG raw o URL imagen) + texto debajo del banner
1397
+ - Nuevos tipos exportados: `ForYouHubBanner`, `ForYouHubBannerHighlight`
1398
+
1399
+ ### 0.18.69
1400
+ - `ForYouPage`: nuevo prop `locale` con 8 idiomas (en, es, fr, pl, nl, tr, ro, ar)
1401
+ - `ForYouPage`: traducciones integradas en el SDK (`getForYouTranslations`)
1402
+ - `ForYouPage`: RTL automático para árabe (`dir="rtl"`)
1403
+ - `ForYouPage`: navegación con `window.history.pushState` + `?feature=<key>` en URL
1404
+ - `ForYouPage`: listener `popstate` para botón "Atrás" del navegador
1405
+ - `ForYouPage`: scroll automático al tope al entrar/salir de features
1406
+ - `ForYouPage`: botón volver con icono SVG configurable
1407
+ - `ForYouPage`: nuevo `ForYouFeatureConfig.apiBaseUrl` — el SDK construye las URLs
1408
+ - `ForYouPage`: nuevo `ForYouFeatureConfig.contents[]` — multi-preset en paralelo
1409
+ - `Wallpapers`: nuevo prop `hideBreadcrumb` — oculta el breadcrumb interno
1410
+ - Todos los hooks: migrados a `forYouFetch` con soporte `contents[]` y `apiBaseUrl`
1411
+ - Nuevo: `forYouFetch` helper exportado independientemente
1412
+ - Nuevo: `getForYouTranslations`, `isRTL`, `FOR_YOU_I18N`, `FOR_YOU_LOCALES`
1413
+ - Nuevo: tipos `ForYouLocale`, `ForYouContentPreset`, `ForYouFetchOptions` y variantes