@jjlmoya/utils-developer 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/package.json +64 -0
  2. package/src/category/i18n/en.ts +34 -0
  3. package/src/category/i18n/es.ts +34 -0
  4. package/src/category/i18n/fr.ts +34 -0
  5. package/src/category/index.ts +31 -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 +78 -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/css_specificity_logic.test.ts +39 -0
  17. package/src/tests/faq_count.test.ts +19 -0
  18. package/src/tests/locale_completeness.test.ts +42 -0
  19. package/src/tests/mocks/astro_mock.js +2 -0
  20. package/src/tests/no_h1_in_components.test.ts +48 -0
  21. package/src/tests/schemas_fulfillment.test.ts +23 -0
  22. package/src/tests/seo_length.test.ts +22 -0
  23. package/src/tests/title_quality.test.ts +55 -0
  24. package/src/tests/tool_validation.test.ts +17 -0
  25. package/src/tool/aspectRatio/bibliography.astro +14 -0
  26. package/src/tool/aspectRatio/component.astro +468 -0
  27. package/src/tool/aspectRatio/i18n/en.ts +177 -0
  28. package/src/tool/aspectRatio/i18n/es.ts +177 -0
  29. package/src/tool/aspectRatio/i18n/fr.ts +177 -0
  30. package/src/tool/aspectRatio/index.ts +34 -0
  31. package/src/tool/aspectRatio/seo.astro +14 -0
  32. package/src/tool/aspectRatio/ui.ts +12 -0
  33. package/src/tool/calculadoraTiempoDatos/bibliography.astro +14 -0
  34. package/src/tool/calculadoraTiempoDatos/component.astro +829 -0
  35. package/src/tool/calculadoraTiempoDatos/i18n/en.ts +150 -0
  36. package/src/tool/calculadoraTiempoDatos/i18n/es.ts +163 -0
  37. package/src/tool/calculadoraTiempoDatos/i18n/fr.ts +150 -0
  38. package/src/tool/calculadoraTiempoDatos/index.ts +33 -0
  39. package/src/tool/calculadoraTiempoDatos/seo.astro +14 -0
  40. package/src/tool/calculadoraTiempoDatos/ui.ts +24 -0
  41. package/src/tool/colorConverter/bibliography.astro +14 -0
  42. package/src/tool/colorConverter/component.astro +504 -0
  43. package/src/tool/colorConverter/i18n/en.ts +126 -0
  44. package/src/tool/colorConverter/i18n/es.ts +126 -0
  45. package/src/tool/colorConverter/i18n/fr.ts +126 -0
  46. package/src/tool/colorConverter/index.ts +33 -0
  47. package/src/tool/colorConverter/seo.astro +14 -0
  48. package/src/tool/colorConverter/ui.ts +16 -0
  49. package/src/tool/conversorExcelCsvHtml/bibliography.astro +14 -0
  50. package/src/tool/conversorExcelCsvHtml/component.astro +595 -0
  51. package/src/tool/conversorExcelCsvHtml/i18n/en.ts +129 -0
  52. package/src/tool/conversorExcelCsvHtml/i18n/es.ts +129 -0
  53. package/src/tool/conversorExcelCsvHtml/i18n/fr.ts +129 -0
  54. package/src/tool/conversorExcelCsvHtml/index.ts +33 -0
  55. package/src/tool/conversorExcelCsvHtml/seo.astro +14 -0
  56. package/src/tool/conversorExcelCsvHtml/ui.ts +14 -0
  57. package/src/tool/cronGenerator/bibliography.astro +14 -0
  58. package/src/tool/cronGenerator/component.astro +667 -0
  59. package/src/tool/cronGenerator/i18n/en.ts +191 -0
  60. package/src/tool/cronGenerator/i18n/es.ts +191 -0
  61. package/src/tool/cronGenerator/i18n/fr.ts +191 -0
  62. package/src/tool/cronGenerator/index.ts +34 -0
  63. package/src/tool/cronGenerator/seo.astro +14 -0
  64. package/src/tool/cronGenerator/ui.ts +45 -0
  65. package/src/tool/cssSpecificityCalculator/bibliography.astro +14 -0
  66. package/src/tool/cssSpecificityCalculator/component.astro +292 -0
  67. package/src/tool/cssSpecificityCalculator/i18n/en.ts +168 -0
  68. package/src/tool/cssSpecificityCalculator/i18n/es.ts +168 -0
  69. package/src/tool/cssSpecificityCalculator/i18n/fr.ts +168 -0
  70. package/src/tool/cssSpecificityCalculator/index.ts +34 -0
  71. package/src/tool/cssSpecificityCalculator/logic.ts +100 -0
  72. package/src/tool/cssSpecificityCalculator/seo.astro +14 -0
  73. package/src/tool/cssSpecificityCalculator/ui-controller.ts +57 -0
  74. package/src/tool/cssSpecificityCalculator/ui.ts +11 -0
  75. package/src/tool/cssToInlineConverter/bibliography.astro +14 -0
  76. package/src/tool/cssToInlineConverter/component.astro +308 -0
  77. package/src/tool/cssToInlineConverter/i18n/en.ts +160 -0
  78. package/src/tool/cssToInlineConverter/i18n/es.ts +160 -0
  79. package/src/tool/cssToInlineConverter/i18n/fr.ts +160 -0
  80. package/src/tool/cssToInlineConverter/index.ts +34 -0
  81. package/src/tool/cssToInlineConverter/seo.astro +14 -0
  82. package/src/tool/cssToInlineConverter/ui.ts +12 -0
  83. package/src/tool/duplicateCssRemover/bibliography.astro +14 -0
  84. package/src/tool/duplicateCssRemover/component.astro +353 -0
  85. package/src/tool/duplicateCssRemover/i18n/en.ts +167 -0
  86. package/src/tool/duplicateCssRemover/i18n/es.ts +167 -0
  87. package/src/tool/duplicateCssRemover/i18n/fr.ts +167 -0
  88. package/src/tool/duplicateCssRemover/index.ts +34 -0
  89. package/src/tool/duplicateCssRemover/seo.astro +14 -0
  90. package/src/tool/duplicateCssRemover/ui.ts +12 -0
  91. package/src/tool/generadorSecurityTxt/bibliography.astro +14 -0
  92. package/src/tool/generadorSecurityTxt/component.astro +473 -0
  93. package/src/tool/generadorSecurityTxt/i18n/en.ts +173 -0
  94. package/src/tool/generadorSecurityTxt/i18n/es.ts +173 -0
  95. package/src/tool/generadorSecurityTxt/i18n/fr.ts +173 -0
  96. package/src/tool/generadorSecurityTxt/index.ts +33 -0
  97. package/src/tool/generadorSecurityTxt/seo.astro +14 -0
  98. package/src/tool/generadorSecurityTxt/ui.ts +27 -0
  99. package/src/tool/hashGenerator/bibliography.astro +14 -0
  100. package/src/tool/hashGenerator/component.astro +367 -0
  101. package/src/tool/hashGenerator/i18n/en.ts +118 -0
  102. package/src/tool/hashGenerator/i18n/es.ts +118 -0
  103. package/src/tool/hashGenerator/i18n/fr.ts +118 -0
  104. package/src/tool/hashGenerator/index.ts +33 -0
  105. package/src/tool/hashGenerator/md5.ts +138 -0
  106. package/src/tool/hashGenerator/seo.astro +14 -0
  107. package/src/tool/hashGenerator/ui.ts +11 -0
  108. package/src/tool/inspectorCertificadosSsl/bibliography.astro +14 -0
  109. package/src/tool/inspectorCertificadosSsl/component.astro +580 -0
  110. package/src/tool/inspectorCertificadosSsl/i18n/en.ts +214 -0
  111. package/src/tool/inspectorCertificadosSsl/i18n/es.ts +214 -0
  112. package/src/tool/inspectorCertificadosSsl/i18n/fr.ts +214 -0
  113. package/src/tool/inspectorCertificadosSsl/index.ts +33 -0
  114. package/src/tool/inspectorCertificadosSsl/seo.astro +14 -0
  115. package/src/tool/inspectorCertificadosSsl/ui.ts +29 -0
  116. package/src/tool/jsonFormatter/bibliography.astro +14 -0
  117. package/src/tool/jsonFormatter/component.astro +590 -0
  118. package/src/tool/jsonFormatter/i18n/en.ts +156 -0
  119. package/src/tool/jsonFormatter/i18n/es.ts +156 -0
  120. package/src/tool/jsonFormatter/i18n/fr.ts +156 -0
  121. package/src/tool/jsonFormatter/index.ts +34 -0
  122. package/src/tool/jsonFormatter/seo.astro +14 -0
  123. package/src/tool/jsonFormatter/ui.ts +15 -0
  124. package/src/tool/keycode/bibliography.astro +14 -0
  125. package/src/tool/keycode/component.astro +540 -0
  126. package/src/tool/keycode/i18n/en.ts +160 -0
  127. package/src/tool/keycode/i18n/es.ts +160 -0
  128. package/src/tool/keycode/i18n/fr.ts +160 -0
  129. package/src/tool/keycode/index.ts +34 -0
  130. package/src/tool/keycode/seo.astro +14 -0
  131. package/src/tool/keycode/ui.ts +10 -0
  132. package/src/tool/llmCostCalculator/bibliography.astro +14 -0
  133. package/src/tool/llmCostCalculator/component.astro +488 -0
  134. package/src/tool/llmCostCalculator/data.ts +99 -0
  135. package/src/tool/llmCostCalculator/i18n/en.ts +165 -0
  136. package/src/tool/llmCostCalculator/i18n/es.ts +165 -0
  137. package/src/tool/llmCostCalculator/i18n/fr.ts +165 -0
  138. package/src/tool/llmCostCalculator/index.ts +34 -0
  139. package/src/tool/llmCostCalculator/seo.astro +14 -0
  140. package/src/tool/llmCostCalculator/ui.ts +11 -0
  141. package/src/tool/mobileMockupGenerator/app.ts +614 -0
  142. package/src/tool/mobileMockupGenerator/bibliography.astro +14 -0
  143. package/src/tool/mobileMockupGenerator/component.astro +1044 -0
  144. package/src/tool/mobileMockupGenerator/constants.ts +48 -0
  145. package/src/tool/mobileMockupGenerator/i18n/en.ts +121 -0
  146. package/src/tool/mobileMockupGenerator/i18n/es.ts +121 -0
  147. package/src/tool/mobileMockupGenerator/i18n/fr.ts +121 -0
  148. package/src/tool/mobileMockupGenerator/index.ts +33 -0
  149. package/src/tool/mobileMockupGenerator/rendering.ts +189 -0
  150. package/src/tool/mobileMockupGenerator/seo.astro +14 -0
  151. package/src/tool/mobileMockupGenerator/store.ts +145 -0
  152. package/src/tool/mobileMockupGenerator/types.ts +41 -0
  153. package/src/tool/mobileMockupGenerator/ui.ts +48 -0
  154. package/src/tool/musicalTypography/bibliography.astro +14 -0
  155. package/src/tool/musicalTypography/component.astro +480 -0
  156. package/src/tool/musicalTypography/i18n/en.ts +179 -0
  157. package/src/tool/musicalTypography/i18n/es.ts +179 -0
  158. package/src/tool/musicalTypography/i18n/fr.ts +179 -0
  159. package/src/tool/musicalTypography/index.ts +33 -0
  160. package/src/tool/musicalTypography/seo.astro +14 -0
  161. package/src/tool/musicalTypography/ui.ts +25 -0
  162. package/src/tool/placeholderGenerator/bibliography.astro +14 -0
  163. package/src/tool/placeholderGenerator/component.astro +499 -0
  164. package/src/tool/placeholderGenerator/i18n/en.ts +176 -0
  165. package/src/tool/placeholderGenerator/i18n/es.ts +176 -0
  166. package/src/tool/placeholderGenerator/i18n/fr.ts +176 -0
  167. package/src/tool/placeholderGenerator/index.ts +34 -0
  168. package/src/tool/placeholderGenerator/seo.astro +14 -0
  169. package/src/tool/placeholderGenerator/ui.ts +14 -0
  170. package/src/tool/promptLibrary/bibliography.astro +14 -0
  171. package/src/tool/promptLibrary/component.astro +1131 -0
  172. package/src/tool/promptLibrary/controller.ts +49 -0
  173. package/src/tool/promptLibrary/i18n/en.ts +131 -0
  174. package/src/tool/promptLibrary/i18n/es.ts +131 -0
  175. package/src/tool/promptLibrary/i18n/fr.ts +126 -0
  176. package/src/tool/promptLibrary/index.ts +33 -0
  177. package/src/tool/promptLibrary/seo.astro +14 -0
  178. package/src/tool/promptLibrary/ui.ts +29 -0
  179. package/src/tool/readabilityCalculator/bibliography.astro +14 -0
  180. package/src/tool/readabilityCalculator/component.astro +534 -0
  181. package/src/tool/readabilityCalculator/i18n/en.ts +133 -0
  182. package/src/tool/readabilityCalculator/i18n/es.ts +133 -0
  183. package/src/tool/readabilityCalculator/i18n/fr.ts +133 -0
  184. package/src/tool/readabilityCalculator/index.ts +33 -0
  185. package/src/tool/readabilityCalculator/seo.astro +14 -0
  186. package/src/tool/readabilityCalculator/ui.ts +27 -0
  187. package/src/tool/svgSanitizer/bibliography.astro +14 -0
  188. package/src/tool/svgSanitizer/component.astro +554 -0
  189. package/src/tool/svgSanitizer/i18n/en.ts +125 -0
  190. package/src/tool/svgSanitizer/i18n/es.ts +125 -0
  191. package/src/tool/svgSanitizer/i18n/fr.ts +125 -0
  192. package/src/tool/svgSanitizer/index.ts +33 -0
  193. package/src/tool/svgSanitizer/seo.astro +14 -0
  194. package/src/tool/svgSanitizer/ui.ts +18 -0
  195. package/src/tool/svgToCss/bibliography.astro +14 -0
  196. package/src/tool/svgToCss/component.astro +381 -0
  197. package/src/tool/svgToCss/i18n/en.ts +174 -0
  198. package/src/tool/svgToCss/i18n/es.ts +174 -0
  199. package/src/tool/svgToCss/i18n/fr.ts +174 -0
  200. package/src/tool/svgToCss/index.ts +34 -0
  201. package/src/tool/svgToCss/seo.astro +14 -0
  202. package/src/tool/svgToCss/ui.ts +13 -0
  203. package/src/tool/urlCleaner/bibliography.astro +14 -0
  204. package/src/tool/urlCleaner/component.astro +458 -0
  205. package/src/tool/urlCleaner/i18n/en.ts +116 -0
  206. package/src/tool/urlCleaner/i18n/es.ts +116 -0
  207. package/src/tool/urlCleaner/i18n/fr.ts +116 -0
  208. package/src/tool/urlCleaner/index.ts +33 -0
  209. package/src/tool/urlCleaner/seo.astro +14 -0
  210. package/src/tool/urlCleaner/ui.ts +12 -0
  211. package/src/tool/urlEncoderDecoder/bibliography.astro +14 -0
  212. package/src/tool/urlEncoderDecoder/component.astro +291 -0
  213. package/src/tool/urlEncoderDecoder/i18n/en.ts +162 -0
  214. package/src/tool/urlEncoderDecoder/i18n/es.ts +162 -0
  215. package/src/tool/urlEncoderDecoder/i18n/fr.ts +162 -0
  216. package/src/tool/urlEncoderDecoder/index.ts +34 -0
  217. package/src/tool/urlEncoderDecoder/seo.astro +14 -0
  218. package/src/tool/urlEncoderDecoder/ui.ts +11 -0
  219. package/src/tool/utmGenerator/bibliography.astro +14 -0
  220. package/src/tool/utmGenerator/component.astro +270 -0
  221. package/src/tool/utmGenerator/i18n/en.ts +117 -0
  222. package/src/tool/utmGenerator/i18n/es.ts +117 -0
  223. package/src/tool/utmGenerator/i18n/fr.ts +117 -0
  224. package/src/tool/utmGenerator/index.ts +33 -0
  225. package/src/tool/utmGenerator/seo.astro +14 -0
  226. package/src/tool/utmGenerator/ui.ts +13 -0
  227. package/src/tools.ts +27 -0
  228. package/src/types.ts +72 -0
@@ -0,0 +1,614 @@
1
+ import JSZip from 'jszip';
2
+ import { saveAs } from 'file-saver';
3
+ import { Store } from './store';
4
+ import { RenderingEngine } from './rendering';
5
+ import { DEVICES, GRADIENTS, EXPORT_WIDTH, EXPORT_HEIGHT, LAYOUT_PRESETS } from './constants';
6
+ import type { MockupImage, MockupVariant } from './types';
7
+ import type { MobileMockupGeneratorUI } from './ui';
8
+
9
+ interface AppRefs {
10
+ root: HTMLElement;
11
+ uploadInput: HTMLInputElement;
12
+ previewGrid: HTMLElement;
13
+ emptyState: HTMLElement;
14
+ fontSelect: HTMLSelectElement;
15
+ gradientStart: HTMLInputElement;
16
+ gradientEnd: HTMLInputElement;
17
+ gradientAngle: HTMLInputElement;
18
+ angleValue: HTMLElement;
19
+ gradientPresets: HTMLElement;
20
+ downloadBtn: HTMLElement;
21
+ clearBtn: HTMLElement;
22
+ deviceBtns: NodeListOf<Element>;
23
+ replaceInput: HTMLInputElement;
24
+ safeAreaToggle: HTMLElement;
25
+ safeAreaDot: HTMLElement;
26
+ safeAreaColorInput: HTMLInputElement;
27
+ massReplaceBtn: HTMLElement;
28
+ massReplaceInput: HTMLInputElement;
29
+ }
30
+
31
+ export class App {
32
+ store = new Store();
33
+ refs!: AppRefs;
34
+ t!: MobileMockupGeneratorUI;
35
+ currentTargetId: string | null = null;
36
+ isAddingVariant = false;
37
+ isReplacingBackground = false;
38
+
39
+ constructor() {
40
+ this.init();
41
+ }
42
+
43
+ private init() {
44
+ this.refs = this.getRefs();
45
+ this.t = JSON.parse(this.refs.root.dataset.i18n ?? '{}') as MobileMockupGeneratorUI;
46
+ this.setupAllListeners();
47
+ this.syncUIWithStore();
48
+ this.renderPresets();
49
+ this.updateVisibility();
50
+ this.renderGrid();
51
+ }
52
+
53
+ private getRefs(): AppRefs {
54
+ return {
55
+ root: document.getElementById('mmg-root')!,
56
+ uploadInput: document.getElementById('mmg-upload') as HTMLInputElement,
57
+ previewGrid: document.getElementById('mmg-grid')!,
58
+ emptyState: document.getElementById('mmg-empty')!,
59
+ fontSelect: document.getElementById('mmg-font') as HTMLSelectElement,
60
+ gradientStart: document.getElementById('mmg-grad-start') as HTMLInputElement,
61
+ gradientEnd: document.getElementById('mmg-grad-end') as HTMLInputElement,
62
+ gradientAngle: document.getElementById('mmg-grad-angle') as HTMLInputElement,
63
+ angleValue: document.getElementById('mmg-angle-val')!,
64
+ gradientPresets: document.getElementById('mmg-presets')!,
65
+ downloadBtn: document.getElementById('mmg-download')!,
66
+ clearBtn: document.getElementById('mmg-clear')!,
67
+ deviceBtns: document.querySelectorAll('[data-device]'),
68
+ replaceInput: document.getElementById('mmg-replace') as HTMLInputElement,
69
+ safeAreaToggle: document.getElementById('mmg-safe-toggle')!,
70
+ safeAreaDot: document.getElementById('mmg-safe-dot')!,
71
+ safeAreaColorInput: document.getElementById('mmg-safe-color') as HTMLInputElement,
72
+ massReplaceBtn: document.getElementById('mmg-mass-btn')!,
73
+ massReplaceInput: document.getElementById('mmg-mass-input') as HTMLInputElement,
74
+ };
75
+ }
76
+
77
+ private setupAllListeners() {
78
+ this.setupUploadZone();
79
+ this.setupColorControls();
80
+ this.setupDeviceAndFont();
81
+ this.setupGlobalControls();
82
+ this.setupSafeArea();
83
+ this.setupGridEvents();
84
+ this.setupReplaceListener();
85
+ }
86
+
87
+ private setupUploadZone() {
88
+ this.refs.uploadInput.addEventListener('change', (e) => this.handleUpload(e));
89
+ this.refs.massReplaceBtn.addEventListener('click', () => this.refs.massReplaceInput.click());
90
+ this.refs.massReplaceInput.addEventListener('change', (e) => this.handleMassReplace(e));
91
+ }
92
+
93
+ private setupColorControls() {
94
+ this.refs.gradientStart.addEventListener('input', () => this.updateColors());
95
+ this.refs.gradientEnd.addEventListener('input', () => this.updateColors());
96
+ this.refs.gradientAngle.addEventListener('input', () => {
97
+ this.refs.angleValue.textContent = `${this.refs.gradientAngle.value}\u00b0`;
98
+ this.updateColors();
99
+ });
100
+ document.getElementById('mmg-swap-colors')?.addEventListener('click', () => {
101
+ const tmp = this.refs.gradientStart.value;
102
+ this.refs.gradientStart.value = this.refs.gradientEnd.value;
103
+ this.refs.gradientEnd.value = tmp;
104
+ this.updateColors();
105
+ });
106
+ }
107
+
108
+ private setupDeviceAndFont() {
109
+ this.refs.fontSelect.addEventListener('change', () => {
110
+ this.store.font = this.refs.fontSelect.value;
111
+ this.store.save();
112
+ this.renderGrid();
113
+ });
114
+ this.refs.deviceBtns.forEach((btn) => {
115
+ btn.addEventListener('click', () => {
116
+ this.store.device = (btn as HTMLElement).dataset.device || 'iphone';
117
+ this.store.save();
118
+ this.updateDeviceButtons();
119
+ this.renderGrid();
120
+ });
121
+ });
122
+ }
123
+
124
+ private setupGlobalControls() {
125
+ this.refs.clearBtn.addEventListener('click', () => {
126
+ if (!confirm(this.t.confirmClear)) return;
127
+ this.store.images = [];
128
+ this.store.save();
129
+ this.updateVisibility();
130
+ this.renderGrid();
131
+ });
132
+ this.refs.downloadBtn.addEventListener('click', () => this.exportZip());
133
+ }
134
+
135
+ private setupSafeArea() {
136
+ this.refs.safeAreaToggle.addEventListener('click', () => {
137
+ this.store.showSafeArea = !this.store.showSafeArea;
138
+ this.store.save();
139
+ this.updateSafeAreaUI();
140
+ this.renderGrid();
141
+ });
142
+ this.refs.safeAreaColorInput.addEventListener('input', () => {
143
+ this.store.safeAreaColor = this.refs.safeAreaColorInput.value;
144
+ this.store.save();
145
+ this.renderGrid();
146
+ });
147
+ }
148
+
149
+ private setupGridEvents() {
150
+ this.refs.previewGrid.addEventListener('input', (e) => this.onGridInput(e));
151
+ this.refs.previewGrid.addEventListener('click', (e) => this.onGridClick(e));
152
+ }
153
+
154
+ private onGridInput(e: Event) {
155
+ const target = e.target as HTMLElement;
156
+ const id = target.dataset.imgId;
157
+ const type = target.dataset.type;
158
+ const img = this.store.images.find((i) => i.id === id);
159
+ if (!img || !type) return;
160
+ const val = (target as HTMLInputElement).value;
161
+ this.applySettingFromInput(img, type, val);
162
+ this.updateRangeLabel(type, id!, val);
163
+ this.refreshSingle(img);
164
+ this.store.save();
165
+ }
166
+
167
+ private applySettingFromInput(img: MockupImage, type: string, val: string) {
168
+ const numFields = ['spacing', 'textSize', 'textRotation', 'textY', 'textX', 'deviceOffsetX', 'deviceOffsetY'];
169
+ if (type === 'text') { img.settings.text = val; return; }
170
+ if (type === 'textColor') { img.settings.textColor = val; return; }
171
+ if (numFields.includes(type)) (img.settings as Record<string, unknown>)[type] = parseInt(val);
172
+ }
173
+
174
+ private updateRangeLabel(type: string, id: string, val: string) {
175
+ const label = this.refs.previewGrid.querySelector(`[data-label-for="${type}-${id}"]`);
176
+ if (!label) return;
177
+ const suffixMap: Record<string, string> = { textSize: 'px', textRotation: '\u00b0' };
178
+ const suffix = suffixMap[type] ?? '%';
179
+ label.textContent = `${val}${suffix}`;
180
+ }
181
+
182
+ private onGridClick(e: Event) {
183
+ this.tryCardAction(e.target as HTMLElement);
184
+ }
185
+
186
+ private tryCardAction(target: HTMLElement) {
187
+ const actions: Array<[string, (el: HTMLElement) => void]> = [
188
+ ['.mmg-delete-img', (el) => this.deleteImage(el.dataset.imgId!)],
189
+ ['.mmg-duplicate-img', (el) => this.duplicateImage(el.dataset.imgId!)],
190
+ ['.mmg-replace-img', (el) => this.startReplace(el.dataset.imgId!, false, false)],
191
+ ['.mmg-replace-bg', (el) => this.startReplace(el.dataset.imgId!, false, true)],
192
+ ['.mmg-add-variant', (el) => this.startReplace(el.dataset.imgId!, true, false)],
193
+ ['.mmg-clear-bg', (el) => this.clearBg(el.dataset.imgId!)],
194
+ ['.mmg-reset-text', (el) => this.resetText(el.dataset.imgId!)],
195
+ ['.mmg-reset-device', (el) => this.resetDevice(el.dataset.imgId!)],
196
+ ['.mmg-preset-btn', (el) => this.applyPreset(el.dataset.imgId!, parseInt(el.dataset.presetIndex ?? '0'))],
197
+ ];
198
+ for (const [sel, fn] of actions) {
199
+ const el = target.closest(sel) as HTMLElement;
200
+ if (el) { fn(el); return; }
201
+ }
202
+ this.tryVariantAction(target);
203
+ }
204
+
205
+ private tryVariantAction(target: HTMLElement) {
206
+ const deleteVariant = target.closest('.mmg-delete-variant') as HTMLElement;
207
+ const variantTab = target.closest('.mmg-variant-tab') as HTMLElement;
208
+ if (deleteVariant) { this.deleteVariant(deleteVariant.dataset.imgId!, deleteVariant.dataset.varId!); return; }
209
+ if (variantTab) this.activateVariant(variantTab.dataset.imgId!, variantTab.dataset.varId!);
210
+ }
211
+
212
+ private deleteImage(id: string) {
213
+ this.store.images = this.store.images.filter((i) => i.id !== id);
214
+ this.store.save();
215
+ this.updateVisibility();
216
+ this.renderGrid();
217
+ }
218
+
219
+ private duplicateImage(id: string) {
220
+ const idx = this.store.images.findIndex((img) => img.id === id);
221
+ const original = this.store.images[idx];
222
+ if (!original) return;
223
+ const copy: MockupImage = {
224
+ ...original,
225
+ id: Math.random().toString(36).slice(2, 11),
226
+ settings: { ...original.settings },
227
+ variants: original.variants.map((v) => ({ ...v, id: Math.random().toString(36).slice(2, 11) })),
228
+ };
229
+ this.store.images.splice(idx + 1, 0, copy);
230
+ this.store.save();
231
+ this.renderGrid();
232
+ }
233
+
234
+ private startReplace(id: string, addVariant: boolean, replaceBg: boolean) {
235
+ this.currentTargetId = id;
236
+ this.isAddingVariant = addVariant;
237
+ this.isReplacingBackground = replaceBg;
238
+ this.refs.replaceInput.click();
239
+ }
240
+
241
+ private clearBg(id: string) {
242
+ const img = this.store.images.find((i) => i.id === id);
243
+ if (!img) return;
244
+ img.settings.bgImage = null;
245
+ this.store.save();
246
+ this.renderGrid();
247
+ }
248
+
249
+ private resetText(id: string) {
250
+ const img = this.store.images.find((i) => i.id === id);
251
+ if (!img) return;
252
+ img.settings.textSize = 28;
253
+ img.settings.textY = 82;
254
+ img.settings.textX = 50;
255
+ img.settings.textRotation = 0;
256
+ img.settings.textColor = '#ffffff';
257
+ this.store.save();
258
+ this.renderGrid();
259
+ }
260
+
261
+ private resetDevice(id: string) {
262
+ const img = this.store.images.find((i) => i.id === id);
263
+ if (!img) return;
264
+ img.settings.spacing = 40;
265
+ img.settings.deviceOffsetX = 0;
266
+ img.settings.deviceOffsetY = 0;
267
+ this.store.save();
268
+ this.renderGrid();
269
+ }
270
+
271
+ private applyPreset(id: string, index: number) {
272
+ const img = this.store.images.find((i) => i.id === id);
273
+ const preset = LAYOUT_PRESETS[index];
274
+ if (!img || !preset) return;
275
+ img.settings.spacing = preset.spacing;
276
+ img.settings.textY = preset.textY;
277
+ img.settings.textX = preset.textX;
278
+ img.settings.textSize = preset.textSize ?? img.settings.textSize;
279
+ img.settings.deviceOffsetX = preset.deviceOffsetX;
280
+ img.settings.deviceOffsetY = preset.deviceOffsetY;
281
+ img.settings.textRotation = preset.rotation;
282
+ this.store.save();
283
+ this.renderGrid();
284
+ }
285
+
286
+ private deleteVariant(imgId: string, varId: string) {
287
+ const img = this.store.images.find((i) => i.id === imgId);
288
+ if (!img || !varId || img.variants.length <= 1) return;
289
+ img.variants = img.variants.filter((v) => v.id !== varId);
290
+ if (img.activeVariantId === varId) img.activeVariantId = img.variants[0].id;
291
+ this.store.save();
292
+ this.renderGrid();
293
+ }
294
+
295
+ private activateVariant(imgId: string, varId: string) {
296
+ const img = this.store.images.find((i) => i.id === imgId);
297
+ if (img && varId) { img.activeVariantId = varId; this.renderGrid(); }
298
+ }
299
+
300
+ private setupReplaceListener() {
301
+ this.refs.replaceInput.addEventListener('change', async (e) => {
302
+ if (!this.currentTargetId) return;
303
+ const file = (e.target as HTMLInputElement).files?.[0];
304
+ if (!file) return;
305
+ const dataUrl = await this.readFile(file);
306
+ const img = this.store.images.find((i) => i.id === this.currentTargetId);
307
+ if (img) await this.applyReplaceResult(img, dataUrl);
308
+ this.store.save();
309
+ this.renderGrid();
310
+ this.refs.replaceInput.value = '';
311
+ this.currentTargetId = null;
312
+ });
313
+ }
314
+
315
+ private async applyReplaceResult(img: MockupImage, dataUrl: string) {
316
+ if (this.isReplacingBackground) { img.settings.bgImage = dataUrl; return; }
317
+ if (this.isAddingVariant) { this.addVariant(img, dataUrl); return; }
318
+ this.replaceActiveVariant(img, dataUrl);
319
+ }
320
+
321
+ private addVariant(img: MockupImage, dataUrl: string) {
322
+ const existing = img.variants.map((v) => v.language);
323
+ const lang = this.getNextLang(existing, img.variants.length);
324
+ const newVariant: MockupVariant = { id: Math.random().toString(36).slice(2, 11), language: lang, dataUrl };
325
+ img.variants.push(newVariant);
326
+ img.activeVariantId = newVariant.id;
327
+ }
328
+
329
+ private getNextLang(existing: string[], count: number): string {
330
+ const candidates = ['EN', 'FR', 'DE', 'IT'];
331
+ const found = candidates.find((l) => !existing.includes(l));
332
+ return found ?? `V${count + 1}`;
333
+ }
334
+
335
+ private replaceActiveVariant(img: MockupImage, dataUrl: string) {
336
+ const variant = img.variants.find((v) => v.id === img.activeVariantId) ?? img.variants[0];
337
+ if (variant) variant.dataUrl = dataUrl;
338
+ }
339
+
340
+ private async handleUpload(e: Event) {
341
+ const files = (e.target as HTMLInputElement).files;
342
+ if (!files) return;
343
+ for (const file of Array.from(files)) {
344
+ const dataUrl = await this.readFile(file);
345
+ this.store.images.push(this.createMockupImage(dataUrl));
346
+ }
347
+ this.store.save();
348
+ this.updateVisibility();
349
+ this.renderGrid();
350
+ }
351
+
352
+ private createMockupImage(dataUrl: string): MockupImage {
353
+ const variantId = Math.random().toString(36).slice(2, 11);
354
+ return {
355
+ id: Math.random().toString(36).slice(2, 11),
356
+ variants: [{ id: variantId, language: 'ES', dataUrl }],
357
+ activeVariantId: variantId,
358
+ settings: { text: '', spacing: 40, textSize: 28, textColor: '#ffffff', textRotation: 0, textY: 82, textX: 50, deviceOffsetX: 0, deviceOffsetY: 0, bgImage: null },
359
+ };
360
+ }
361
+
362
+ private async handleMassReplace(e: Event) {
363
+ const files = (e.target as HTMLInputElement).files;
364
+ if (!files) return;
365
+ const fileList = Array.from(files);
366
+ for (let i = 0; i < fileList.length; i++) {
367
+ const dataUrl = await this.readFile(fileList[i]);
368
+ await this.applyMassReplaceFile(dataUrl, i);
369
+ }
370
+ this.store.save();
371
+ this.updateVisibility();
372
+ this.renderGrid();
373
+ this.refs.massReplaceInput.value = '';
374
+ }
375
+
376
+ private async applyMassReplaceFile(dataUrl: string, index: number) {
377
+ if (this.store.images[index]) { this.replaceActiveVariant(this.store.images[index], dataUrl); return; }
378
+ this.store.images.push(this.createMockupImage(dataUrl));
379
+ }
380
+
381
+ private updateColors() {
382
+ this.store.gradient = {
383
+ start: this.refs.gradientStart.value,
384
+ end: this.refs.gradientEnd.value,
385
+ angle: parseInt(this.refs.gradientAngle.value),
386
+ };
387
+ this.store.save();
388
+ this.refreshAll();
389
+ }
390
+
391
+ private updateSafeAreaUI() {
392
+ this.refs.safeAreaToggle.classList.toggle('mmg-toggle-on', this.store.showSafeArea);
393
+ this.refs.safeAreaDot.style.transform = this.store.showSafeArea ? 'translateX(1.25rem)' : 'translateX(0)';
394
+ }
395
+
396
+ private syncUIWithStore() {
397
+ this.refs.fontSelect.value = this.store.font;
398
+ this.refs.gradientStart.value = this.store.gradient.start;
399
+ this.refs.gradientEnd.value = this.store.gradient.end;
400
+ this.refs.gradientAngle.value = this.store.gradient.angle.toString();
401
+ this.refs.angleValue.textContent = `${this.store.gradient.angle}\u00b0`;
402
+ this.refs.safeAreaColorInput.value = this.store.safeAreaColor;
403
+ this.updateSafeAreaUI();
404
+ this.updateDeviceButtons();
405
+ }
406
+
407
+ private updateDeviceButtons() {
408
+ this.refs.deviceBtns.forEach((btn) => {
409
+ btn.classList.toggle('mmg-device-active', (btn as HTMLElement).dataset.device === this.store.device);
410
+ });
411
+ }
412
+
413
+ private renderPresets() {
414
+ this.refs.gradientPresets.innerHTML = GRADIENTS.map((g) => `<button class="mmg-preset-swatch" style="background:linear-gradient(135deg,${g.start},${g.end})" data-start="${g.start}" data-end="${g.end}"></button>`).join('');
415
+ this.refs.gradientPresets.querySelectorAll('button').forEach((btn) => {
416
+ btn.addEventListener('click', () => {
417
+ this.refs.gradientStart.value = (btn as HTMLButtonElement).dataset.start!;
418
+ this.refs.gradientEnd.value = (btn as HTMLButtonElement).dataset.end!;
419
+ this.updateColors();
420
+ });
421
+ });
422
+ }
423
+
424
+ private updateVisibility() {
425
+ const has = this.store.images.length > 0;
426
+ this.refs.emptyState.style.display = has ? 'none' : '';
427
+ this.refs.previewGrid.style.display = has ? '' : 'none';
428
+ this.refs.downloadBtn.style.display = has ? '' : 'none';
429
+ this.refs.clearBtn.style.display = has ? '' : 'none';
430
+ }
431
+
432
+ renderGrid() {
433
+ this.refs.previewGrid.innerHTML = this.store.images.map((img) => this.renderCard(img)).join('');
434
+ this.refreshAll();
435
+ }
436
+
437
+ private renderCard(img: MockupImage): string {
438
+ return `<div class="mmg-preview-card">
439
+ <div class="mmg-card-actions">
440
+ <button data-img-id="${img.id}" class="mmg-card-action-btn mmg-duplicate-img" title="${this.t.cardTitleDuplicate}">${this.svgDuplicate()}</button>
441
+ <button data-img-id="${img.id}" class="mmg-card-action-btn mmg-replace-img" title="${this.t.cardTitleReplace}">${this.svgUpload()}</button>
442
+ </div>
443
+ <div class="mmg-canvas-wrap"><canvas id="canvas-${img.id}" class="mmg-canvas"></canvas></div>
444
+ <div class="mmg-card-body">
445
+ ${this.renderLayoutPresets(img)}
446
+ ${this.renderBrandingSection(img)}
447
+ ${this.renderDeviceSection(img)}
448
+ ${this.renderSceneSection(img)}
449
+ <button data-img-id="${img.id}" class="mmg-delete-img mmg-delete-btn">${this.t.cardBtnDeleteScene}</button>
450
+ </div>
451
+ </div>`;
452
+ }
453
+
454
+ private renderLayoutPresets(img: MockupImage): string {
455
+ const presetNames: Record<string, string> = {
456
+ classic: this.t.presetClassic, mobileBottom: this.t.presetMobileBottom,
457
+ mobileTop: this.t.presetMobileTop, focus: this.t.presetFocus,
458
+ dynamic: this.t.presetDynamic, splitLeft: this.t.presetSplitLeft,
459
+ splitRight: this.t.presetSplitRight, imageLeft: this.t.presetImageLeft,
460
+ imageRight: this.t.presetImageRight,
461
+ };
462
+ return `<div class="mmg-section">
463
+ <h4 class="mmg-section-label">${this.t.cardSectionLayouts}</h4>
464
+ <div class="mmg-presets-grid">${LAYOUT_PRESETS.map((p, idx) => `<button class="mmg-preset-btn" data-img-id="${img.id}" data-preset-index="${idx}">${presetNames[p.name] ?? p.name}</button>`).join('')}</div>
465
+ </div>`;
466
+ }
467
+
468
+ private renderBrandingSection(img: MockupImage): string {
469
+ return `<div class="mmg-section">
470
+ <div class="mmg-section-head">
471
+ <h4 class="mmg-section-label">${this.t.cardSectionBranding}</h4>
472
+ <button data-img-id="${img.id}" class="mmg-reset-text mmg-reset-btn" title="${this.t.cardTitleResetText}">${this.svgReset()}</button>
473
+ </div>
474
+ <div class="mmg-color-row">
475
+ <span class="mmg-color-sublabel">${this.t.cardLabelColor}</span>
476
+ <div class="mmg-color-field">
477
+ <input type="color" value="${img.settings.textColor}" data-img-id="${img.id}" data-type="textColor" class="mmg-color-input" />
478
+ <span class="mmg-color-hex">${img.settings.textColor.toUpperCase()}</span>
479
+ </div>
480
+ </div>
481
+ <textarea data-img-id="${img.id}" data-type="text" placeholder="${this.t.cardTextPlaceholder}" class="mmg-textarea">${img.settings.text}</textarea>
482
+ <div class="mmg-range-grid">
483
+ ${this.renderRangeInput(img, 'textSize', this.t.cardRangeLabelSize, 10, 120, 'px')}
484
+ ${this.renderRangeInput(img, 'textX', this.t.cardRangeLabelX, 0, 100, '%')}
485
+ ${this.renderRangeInput(img, 'textY', this.t.cardRangeLabelY, 0, 100, '%')}
486
+ ${this.renderRangeInput(img, 'textRotation', this.t.cardRangeLabelRotation, -90, 90, '\u00b0')}
487
+ </div>
488
+ </div>`;
489
+ }
490
+
491
+ private renderDeviceSection(img: MockupImage): string {
492
+ return `<div class="mmg-section">
493
+ <div class="mmg-section-head">
494
+ <h4 class="mmg-section-label">${this.t.cardSectionDevice}</h4>
495
+ <button data-img-id="${img.id}" class="mmg-reset-device mmg-reset-btn" title="${this.t.cardTitleResetDevice}">${this.svgReset()}</button>
496
+ </div>
497
+ <div class="mmg-range-grid">
498
+ ${this.renderRangeInput(img, 'spacing', this.t.cardRangeLabelScale, 0, 100, '%')}
499
+ ${this.renderRangeInput(img, 'deviceOffsetX', this.t.cardRangeLabelX, -50, 50, '%')}
500
+ ${this.renderRangeInput(img, 'deviceOffsetY', this.t.cardRangeLabelY, -50, 50, '%')}
501
+ </div>
502
+ </div>`;
503
+ }
504
+
505
+ private renderSceneSection(img: MockupImage): string {
506
+ const clearBtn = img.settings.bgImage ? `<button data-img-id="${img.id}" class="mmg-clear-bg mmg-clear-bg-btn">${this.svgTrash()}</button>` : '';
507
+ return `<div class="mmg-section">
508
+ <h4 class="mmg-section-label">${this.t.cardSectionScene}</h4>
509
+ <div class="mmg-scene-row">
510
+ <button data-img-id="${img.id}" class="mmg-replace-bg mmg-scene-btn">${this.svgImage()} ${this.t.cardBtnSpecialBg}</button>
511
+ ${clearBtn}
512
+ </div>
513
+ </div>`;
514
+ }
515
+
516
+ private renderRangeInput(img: MockupImage, type: string, label: string, min: number, max: number, suffix: string): string {
517
+ const val = (img.settings as Record<string, unknown>)[type] as number;
518
+ return `<div class="mmg-range-item">
519
+ <div class="mmg-range-row">
520
+ <span>${label}</span>
521
+ <span data-label-for="${type}-${img.id}">${val}${suffix}</span>
522
+ </div>
523
+ <input type="range" min="${min}" max="${max}" value="${val}" data-img-id="${img.id}" data-type="${type}" class="mmg-range" />
524
+ </div>`;
525
+ }
526
+
527
+ private refreshAll() {
528
+ this.store.images.forEach((img) => this.refreshSingle(img));
529
+ }
530
+
531
+ private refreshSingle(image: MockupImage) {
532
+ const canvas = document.getElementById(`canvas-${image.id}`) as HTMLCanvasElement;
533
+ if (!canvas) return;
534
+ const variant = image.variants.find((v) => v.id === image.activeVariantId) ?? image.variants[0];
535
+ if (!variant) return;
536
+ RenderingEngine.drawMockup(canvas, image, {
537
+ dataUrl: variant.dataUrl,
538
+ device: DEVICES[this.store.device],
539
+ gs: { gradient: this.store.gradient, font: this.store.font, safeArea: { show: this.store.showSafeArea, color: this.store.safeAreaColor } },
540
+ });
541
+ }
542
+
543
+ private async exportZip() {
544
+ const zip = new JSZip();
545
+ const originalHTML = this.refs.downloadBtn.innerHTML;
546
+ this.refs.downloadBtn.innerHTML = this.t.processingExport;
547
+ this.refs.downloadBtn.setAttribute('disabled', 'true');
548
+ const canvas = document.createElement('canvas');
549
+ canvas.width = EXPORT_WIDTH;
550
+ canvas.height = EXPORT_HEIGHT;
551
+ await this.renderAllToZip(zip, canvas);
552
+ await this.saveZip(zip);
553
+ this.refs.downloadBtn.innerHTML = this.t.exportDone;
554
+ setTimeout(() => {
555
+ this.refs.downloadBtn.innerHTML = originalHTML;
556
+ this.refs.downloadBtn.removeAttribute('disabled');
557
+ }, 2000);
558
+ }
559
+
560
+ private async renderAllToZip(zip: JSZip, canvas: HTMLCanvasElement) {
561
+ for (let i = 0; i < this.store.images.length; i++) {
562
+ const img = this.store.images[i];
563
+ for (const variant of img.variants) await this.renderVariantToZip(zip, img, variant, canvas, i);
564
+ }
565
+ }
566
+
567
+ private async renderVariantToZip(zip: JSZip, img: MockupImage, variant: MockupVariant, canvas: HTMLCanvasElement, idx: number) {
568
+ await RenderingEngine.drawMockup(canvas, img, {
569
+ dataUrl: variant.dataUrl,
570
+ device: DEVICES[this.store.device],
571
+ gs: { gradient: this.store.gradient, font: this.store.font, safeArea: { show: this.store.showSafeArea, color: this.store.safeAreaColor } },
572
+ isExport: true,
573
+ });
574
+ const blob = await new Promise<Blob>((resolve) => canvas.toBlob((b) => resolve(b!), 'image/webp', 1.0));
575
+ const suffix = img.variants.length > 1 ? `-${variant.language.toUpperCase()}` : '';
576
+ zip.file(`mockup-${idx + 1}${suffix}.webp`, blob);
577
+ }
578
+
579
+ private async saveZip(zip: JSZip) {
580
+ const ts = new Date().toISOString().replace(/[:\-]|\..+/g, '').replace('T', '_');
581
+ const tag = this.store.device === 'iphone' ? 'ios' : 'android';
582
+ const content = await zip.generateAsync({ type: 'blob' });
583
+ saveAs(content, `${ts}-store-mocks-${tag}.zip`);
584
+ }
585
+
586
+ private readFile(file: File): Promise<string> {
587
+ return new Promise((r) => {
588
+ const reader = new FileReader();
589
+ reader.onload = (e) => r(e.target?.result as string);
590
+ reader.readAsDataURL(file);
591
+ });
592
+ }
593
+
594
+ private svgDuplicate(): string {
595
+ return '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"/></svg>';
596
+ }
597
+
598
+ private svgUpload(): string {
599
+ return '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>';
600
+ }
601
+
602
+ private svgReset(): string {
603
+ return '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>';
604
+ }
605
+
606
+ private svgTrash(): string {
607
+ return '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>';
608
+ }
609
+
610
+ private svgImage(): string {
611
+ return '<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>';
612
+ }
613
+
614
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { mobileMockupGenerator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await mobileMockupGenerator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}