@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,48 @@
1
+ import type { DeviceConfig, Gradient } from './types';
2
+
3
+ export const CANVAS_ASPECT = 9 / 16;
4
+ export const EXPORT_WIDTH = 1080;
5
+ export const EXPORT_HEIGHT = 1920;
6
+
7
+ export const DEVICES: Record<string, DeviceConfig> = {
8
+ iphone: {
9
+ width: 430,
10
+ height: 932,
11
+ radius: 55,
12
+ notch: true,
13
+ name: 'iPhone 15 Pro Max',
14
+ safeAreaTop: 54,
15
+ safeAreaBottom: 34,
16
+ },
17
+ pixel: {
18
+ width: 412,
19
+ height: 915,
20
+ radius: 32,
21
+ notch: false,
22
+ name: 'Google Pixel 8',
23
+ safeAreaTop: 36,
24
+ safeAreaBottom: 15,
25
+ },
26
+ };
27
+
28
+ export const GRADIENTS: Gradient[] = [
29
+ { start: '#6366f1', end: '#a855f7', angle: 135 },
30
+ { start: '#f43f5e', end: '#fb923c', angle: 135 },
31
+ { start: '#0ea5e9', end: '#22c55e', angle: 135 },
32
+ { start: '#8b5cf6', end: '#ec4899', angle: 135 },
33
+ { start: '#1e293b', end: '#475569', angle: 135 },
34
+ ];
35
+
36
+ export const GOOGLE_FONTS = ['Inter', 'Outfit', 'Poppins', 'Montserrat', 'Playfair Display'];
37
+
38
+ export const LAYOUT_PRESETS = [
39
+ { name: 'classic', spacing: 40, textY: 82, textX: 50, textSize: 28, deviceOffsetX: 0, deviceOffsetY: 0, rotation: 0 },
40
+ { name: 'mobileBottom', spacing: 40, textY: 13, textX: 50, textSize: 28, deviceOffsetX: 0, deviceOffsetY: 8, rotation: 0 },
41
+ { name: 'mobileTop', spacing: 40, textY: 89, textX: 50, textSize: 28, deviceOffsetX: 0, deviceOffsetY: -7, rotation: 0 },
42
+ { name: 'focus', spacing: 70, textY: 10, textX: 50, textSize: 28, deviceOffsetX: 0, deviceOffsetY: 10, rotation: 0 },
43
+ { name: 'dynamic', spacing: 45, textY: 85, textX: 50, textSize: 28, deviceOffsetX: 0, deviceOffsetY: -5, rotation: -10 },
44
+ { name: 'splitLeft', spacing: 55, textY: 50, textX: 50, textSize: 28, deviceOffsetX: 40, deviceOffsetY: 0, rotation: 0 },
45
+ { name: 'splitRight', spacing: 55, textY: 50, textX: 50, textSize: 28, deviceOffsetX: -40, deviceOffsetY: 0, rotation: 0 },
46
+ { name: 'imageLeft', spacing: 0, textY: 50, textX: 50, textSize: 44, deviceOffsetX: 50, deviceOffsetY: 0, rotation: -90 },
47
+ { name: 'imageRight', spacing: 0, textY: 50, textX: 50, textSize: 44, deviceOffsetX: -50, deviceOffsetY: 0, rotation: 90 },
48
+ ];
@@ -0,0 +1,121 @@
1
+ import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { MobileMockupGeneratorUI } from '../ui';
4
+
5
+ const slug = 'mobile-mockup-generator';
6
+ const title = 'Mobile Mockup Generator for App Store. iPhone and Google Pixel';
7
+ const description = 'Create professional presentations for the App Store and Google Play. iPhone and Pixel mockups with custom backgrounds, panoramic layouts, and bulk high-resolution export.';
8
+
9
+ const faqData = [
10
+ { question: 'Are these screenshots valid for the App Store and Google Play?', answer: 'Yes, the generated images meet the proportions and quality requirements of the app stores. Just choose the right device (iPhone for iOS, Pixel for Android) before exporting.' },
11
+ { question: 'Can I use my own custom backgrounds?', answer: 'Absolutely. In addition to our premium gradient library, you can upload any image from your computer to serve as the background environment for your mockups.' },
12
+ { question: 'Is it free for commercial use?', answer: 'Yes, you can use the generated mockups for commercial projects, portfolios or presentations at no cost and with no watermarks.' },
13
+ { question: 'Are my screenshots saved to any server?', answer: 'No. The generator works 100% locally in your browser. Your private images never leave your computer.' },
14
+ ];
15
+
16
+ const howToData = [
17
+ { name: 'Upload screenshots', text: 'Drag or select the screenshots of your mobile app that you want to present.' },
18
+ { name: 'Choose device', text: 'Select the smartphone model (iPhone 15 Pro Max or Google Pixel 8) according to the target store.' },
19
+ { name: 'Customize environment', text: 'Adjust the background, device angle, add marketing text and choose the composition layout.' },
20
+ { name: 'Download in HD', text: 'Export all results in high-resolution WebP format ready to upload to the app store.' },
21
+ ];
22
+
23
+ const faqSchema: WithContext<FAQPage> = {
24
+ '@context': 'https://schema.org',
25
+ '@type': 'FAQPage',
26
+ mainEntity: faqData.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })),
27
+ };
28
+
29
+ const howToSchema: WithContext<HowToThing> = {
30
+ '@context': 'https://schema.org',
31
+ '@type': 'HowTo',
32
+ name: title,
33
+ description,
34
+ step: howToData.map((step, i) => ({ '@type': 'HowToStep', position: i + 1, name: step.name, text: step.text })),
35
+ };
36
+
37
+ const appSchema: WithContext<SoftwareApplication> = {
38
+ '@context': 'https://schema.org',
39
+ '@type': 'SoftwareApplication',
40
+ name: title,
41
+ description,
42
+ applicationCategory: 'DesignApplication',
43
+ operatingSystem: 'All',
44
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
45
+ inLanguage: 'en',
46
+ };
47
+
48
+ const ui: MobileMockupGeneratorUI = {
49
+ labelUpload: 'Upload Screenshots',
50
+ dropHint: 'Drag your images here',
51
+ dropFormats: 'PNG, JPG or WEBP',
52
+ btnMassReplace: 'Mass Replace (Batch)',
53
+ massReplaceHint: 'Replaces current images while keeping your presets and text. Ideal for quick iterations.',
54
+ labelSettings: 'Global Settings',
55
+ labelDevice: 'Device',
56
+ labelFont: 'Typography',
57
+ labelBackground: 'Background',
58
+ titleSwapColors: 'Swap colors',
59
+ labelAngle: 'Angle',
60
+ labelSafeArea: 'Safe Area (Top/Bottom)',
61
+ labelSafeAreaColor: 'Area color',
62
+ emptyTitle: 'No images yet',
63
+ emptySubtitle: 'Upload your screenshots to start designing',
64
+ btnClearAll: 'Clear Canvas',
65
+ btnExport: 'Export All (.zip)',
66
+ cardTitleDuplicate: 'Duplicate Scene',
67
+ cardTitleReplace: 'Replace Current Screenshot',
68
+ cardSectionLayouts: 'Master Layouts',
69
+ cardSectionBranding: 'Branding & Copy',
70
+ cardTitleResetText: 'Reset Text',
71
+ cardLabelColor: 'Color',
72
+ cardTextPlaceholder: 'Write your copy here...',
73
+ cardSectionDevice: 'Device Layout',
74
+ cardTitleResetDevice: 'Reset Position',
75
+ cardSectionScene: 'Props & Background',
76
+ cardBtnSpecialBg: 'Special Background',
77
+ cardBtnDeleteScene: 'Delete Scene',
78
+ cardRangeLabelSize: 'Size',
79
+ cardRangeLabelX: 'X Axis',
80
+ cardRangeLabelY: 'Y Axis',
81
+ cardRangeLabelRotation: 'Rotation',
82
+ cardRangeLabelScale: 'Scale',
83
+ presetClassic: 'Classic',
84
+ presetMobileBottom: 'Mobile Bottom',
85
+ presetMobileTop: 'Mobile Top',
86
+ presetFocus: 'Focus',
87
+ presetDynamic: 'Dynamic',
88
+ presetSplitLeft: 'Split Left',
89
+ presetSplitRight: 'Split Right',
90
+ presetImageLeft: 'Image Left',
91
+ presetImageRight: 'Image Right',
92
+ confirmClear: 'Are you sure you want to delete all mockups?',
93
+ processingExport: 'Processing...',
94
+ exportDone: 'Done!',
95
+ };
96
+
97
+ export const content: ToolLocaleContent<MobileMockupGeneratorUI> = {
98
+ slug,
99
+ title,
100
+ description,
101
+ ui,
102
+ faqTitle: 'Frequently Asked Questions',
103
+ faq: faqData,
104
+ bibliographyTitle: 'References',
105
+ bibliography: [
106
+ { name: 'Apple App Store Screenshot Requirements', url: 'https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications' },
107
+ { name: 'Google Play Screenshot Requirements', url: 'https://support.google.com/googleplay/android-developer/answer/9866151' },
108
+ ],
109
+ howTo: howToData,
110
+ schemas: [faqSchema, howToSchema, appSchema],
111
+ seo: [
112
+ { type: 'title', text: 'Elevate your screenshots to the next level', level: 2 },
113
+ { type: 'paragraph', html: 'Don\'t limit yourself to flat screenshots. Our mockup tool allows developers and designers to create high-impact visual assets without needing Photoshop or Figma.' },
114
+ { type: 'title', text: 'Power with Master Layouts', level: 3 },
115
+ { type: 'grid', columns: 2 },
116
+ { type: 'card', title: 'App Store & Google Play', html: '<p>Optimize your conversion rate. iPhone 15 Pro Max and Pixel 8 mockups are the world standard for app store listings.</p>' },
117
+ { type: 'card', title: 'Pitch Decks & Marketing', html: '<p>Present your ideas with authority. Perfect for investor presentations, social media campaigns and professional UI/UX design portfolios.</p>' },
118
+ { type: 'title', text: 'Professional workflow', level: 3 },
119
+ { type: 'tip', html: 'Use the "Split Left" and "Split Right" presets to create continuous mockups that slide from one image to another in Instagram carousels or App Store screenshots.' },
120
+ ],
121
+ };
@@ -0,0 +1,121 @@
1
+ import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { MobileMockupGeneratorUI } from '../ui';
4
+
5
+ const slug = 'generador-mockups-movil';
6
+ const title = 'Generador de Mockups para App Store. iPhone y Google Pixel';
7
+ const description = 'Crea presentaciones profesionales para la App Store y Google Play. Mockups de iPhone y Pixel con fondos personalizados, layouts panorámicos y exportación masiva en alta resolución.';
8
+
9
+ const faqData = [
10
+ { question: '¿Son estas capturas válidas para la App Store y Google Play?', answer: 'Sí, las imágenes generadas cumplen con las proporciones y calidades requeridas por las tiendas de aplicaciones. Solo tienes que elegir el dispositivo adecuado (iPhone para iOS, Pixel para Android) antes de exportar.' },
11
+ { question: '¿Puedo usar mis propios fondos personalizados?', answer: 'Claro. Además de nuestra biblioteca de gradientes premium, puedes subir cualquier imagen desde tu ordenador para que sirva de ambiente de fondo para tus mockups.' },
12
+ { question: '¿Es gratis para uso comercial?', answer: 'Sí, puedes usar los mockups generados para tus proyectos comerciales, portafolios o presentaciones sin coste alguno y sin marcas de agua.' },
13
+ { question: '¿Se guardan mis capturas de pantalla en algún servidor?', answer: 'No. El generador funciona 100% localmente en tu navegador. Tus imágenes privadas nunca salen de tu ordenador.' },
14
+ ];
15
+
16
+ const howToData = [
17
+ { name: 'Subir capturas', text: 'Arrastra o selecciona las capturas de pantalla de tu aplicación móvil que quieras presentar.' },
18
+ { name: 'Elegir dispositivo', text: 'Selecciona el modelo de smartphone (iPhone 15 Pro Max o Google Pixel 8) según la tienda objetivo.' },
19
+ { name: 'Personalizar entorno', text: 'Ajusta el fondo, el ángulo del dispositivo, añade textos de marketing y elige el layout de la composición.' },
20
+ { name: 'Descargar en HD', text: 'Exporta todos los resultados en formato WebP de alta resolución listos para subir a la tienda de aplicaciones.' },
21
+ ];
22
+
23
+ const faqSchema: WithContext<FAQPage> = {
24
+ '@context': 'https://schema.org',
25
+ '@type': 'FAQPage',
26
+ mainEntity: faqData.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })),
27
+ };
28
+
29
+ const howToSchema: WithContext<HowToThing> = {
30
+ '@context': 'https://schema.org',
31
+ '@type': 'HowTo',
32
+ name: title,
33
+ description,
34
+ step: howToData.map((step, i) => ({ '@type': 'HowToStep', position: i + 1, name: step.name, text: step.text })),
35
+ };
36
+
37
+ const appSchema: WithContext<SoftwareApplication> = {
38
+ '@context': 'https://schema.org',
39
+ '@type': 'SoftwareApplication',
40
+ name: title,
41
+ description,
42
+ applicationCategory: 'DesignApplication',
43
+ operatingSystem: 'All',
44
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
45
+ inLanguage: 'es',
46
+ };
47
+
48
+ const ui: MobileMockupGeneratorUI = {
49
+ labelUpload: 'Subir Capturas',
50
+ dropHint: 'Arrastra tus imágenes aquí',
51
+ dropFormats: 'PNG, JPG o WEBP',
52
+ btnMassReplace: 'Mass Replace (Lote)',
53
+ massReplaceHint: 'Reemplaza las imágenes actuales manteniendo tus presets y textos. Ideal para iteraciones rápidas.',
54
+ labelSettings: 'Configuración Global',
55
+ labelDevice: 'Dispositivo',
56
+ labelFont: 'Tipografía',
57
+ labelBackground: 'Fondo',
58
+ titleSwapColors: 'Intercambiar colores',
59
+ labelAngle: 'Ángulo',
60
+ labelSafeArea: 'Safe Area (Top/Bottom)',
61
+ labelSafeAreaColor: 'Color del área',
62
+ emptyTitle: 'Aún no hay imágenes',
63
+ emptySubtitle: 'Sube tus capturas para empezar a diseñar',
64
+ btnClearAll: 'Limpiar Lienzo',
65
+ btnExport: 'Exportar Todo (.zip)',
66
+ cardTitleDuplicate: 'Duplicar Escena',
67
+ cardTitleReplace: 'Sustituir Captura Actual',
68
+ cardSectionLayouts: 'Layouts Maestros',
69
+ cardSectionBranding: 'Branding & Copys',
70
+ cardTitleResetText: 'Resetear Texto',
71
+ cardLabelColor: 'Color',
72
+ cardTextPlaceholder: 'Escribe el copy aquí...',
73
+ cardSectionDevice: 'Layout Dispositivo',
74
+ cardTitleResetDevice: 'Resetear Posición',
75
+ cardSectionScene: 'Atrezzo & Fondo',
76
+ cardBtnSpecialBg: 'Fondo Especial',
77
+ cardBtnDeleteScene: 'Eliminar Escena',
78
+ cardRangeLabelSize: 'Tamaño',
79
+ cardRangeLabelX: 'Eje X',
80
+ cardRangeLabelY: 'Eje Y',
81
+ cardRangeLabelRotation: 'Giro',
82
+ cardRangeLabelScale: 'Escala',
83
+ presetClassic: 'Clásico',
84
+ presetMobileBottom: 'Móvil Abajo',
85
+ presetMobileTop: 'Móvil Arriba',
86
+ presetFocus: 'Enfoque',
87
+ presetDynamic: 'Dynamic',
88
+ presetSplitLeft: 'Partida Izq.',
89
+ presetSplitRight: 'Partida Der.',
90
+ presetImageLeft: 'Imagen Izq.',
91
+ presetImageRight: 'Imagen Der.',
92
+ confirmClear: '¿Estás seguro de eliminar todos los mockups?',
93
+ processingExport: 'Procesando...',
94
+ exportDone: 'Listo!',
95
+ };
96
+
97
+ export const content: ToolLocaleContent<MobileMockupGeneratorUI> = {
98
+ slug,
99
+ title,
100
+ description,
101
+ ui,
102
+ faqTitle: 'Preguntas Frecuentes',
103
+ faq: faqData,
104
+ bibliographyTitle: 'Referencias',
105
+ bibliography: [
106
+ { name: 'Apple App Store Screenshot Requirements', url: 'https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications' },
107
+ { name: 'Google Play Screenshot Requirements', url: 'https://support.google.com/googleplay/android-developer/answer/9866151' },
108
+ ],
109
+ howTo: howToData,
110
+ schemas: [faqSchema, howToSchema, appSchema],
111
+ seo: [
112
+ { type: 'title', text: 'Eleva tus capturas al siguiente nivel', level: 2 },
113
+ { type: 'paragraph', html: 'No te limites a subir capturas de pantalla planas. Nuestra herramienta de mockups permite a desarrolladores y diseñadores crear activos visuales de alto impacto sin necesidad de Photoshop o Figma.' },
114
+ { type: 'title', text: 'Potencia con Layouts Maestros', level: 3 },
115
+ { type: 'grid', columns: 2 },
116
+ { type: 'card', title: 'App Store y Google Play', html: '<p>Optimiza tu tasa de conversión. Los mockups de iPhone 15 Pro Max y Pixel 8 son el estándar mundial para las fichas de las tiendas de aplicaciones.</p>' },
117
+ { type: 'card', title: 'Pitch Decks y Marketing', html: '<p>Presenta tus ideas con autoridad. Ideal para presentaciones a inversores, campañas en redes sociales y portfolios profesionales de diseño UI/UX.</p>' },
118
+ { type: 'title', text: 'Flujo de trabajo profesional', level: 3 },
119
+ { type: 'tip', html: 'Usa los presets de "Partida Izquierda" y "Partida Derecha" para crear mockups continuos que se deslizan de una imagen a otra en carruseles de Instagram o capturas de la App Store.' },
120
+ ],
121
+ };
@@ -0,0 +1,121 @@
1
+ import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { MobileMockupGeneratorUI } from '../ui';
4
+
5
+ const slug = 'generateur-mockups-mobile';
6
+ const title = 'Générateur de Mockups Mobile pour App Store. iPhone et Google Pixel';
7
+ const description = 'Créez des présentations professionnelles pour l\'App Store et Google Play. Mockups iPhone et Pixel avec arrière-plans personnalisés, mises en page panoramiques et export haute résolution en masse.';
8
+
9
+ const faqData = [
10
+ { question: 'Ces captures sont-elles valides pour l\'App Store et Google Play ?', answer: 'Oui, les images générées respectent les proportions et qualités requises par les boutiques d\'applications. Choisissez simplement le bon appareil (iPhone pour iOS, Pixel pour Android) avant d\'exporter.' },
11
+ { question: 'Puis-je utiliser mes propres arrière-plans personnalisés ?', answer: 'Bien sûr. En plus de notre bibliothèque de dégradés premium, vous pouvez télécharger n\'importe quelle image de votre ordinateur pour servir d\'environnement de fond à vos mockups.' },
12
+ { question: 'Est-ce gratuit pour un usage commercial ?', answer: 'Oui, vous pouvez utiliser les mockups générés pour vos projets commerciaux, portfolios ou présentations sans aucun coût ni filigrane.' },
13
+ { question: 'Mes captures d\'écran sont-elles sauvegardées sur un serveur ?', answer: 'Non. Le générateur fonctionne 100% localement dans votre navigateur. Vos images privées ne quittent jamais votre ordinateur.' },
14
+ ];
15
+
16
+ const howToData = [
17
+ { name: 'Télécharger les captures', text: 'Glissez ou sélectionnez les captures d\'écran de votre application mobile que vous souhaitez présenter.' },
18
+ { name: 'Choisir l\'appareil', text: 'Sélectionnez le modèle de smartphone (iPhone 15 Pro Max ou Google Pixel 8) selon la boutique cible.' },
19
+ { name: 'Personnaliser l\'environnement', text: 'Ajustez l\'arrière-plan, l\'angle de l\'appareil, ajoutez du texte marketing et choisissez la mise en page de la composition.' },
20
+ { name: 'Télécharger en HD', text: 'Exportez tous les résultats au format WebP haute résolution prêts à être uploadés sur la boutique d\'applications.' },
21
+ ];
22
+
23
+ const faqSchema: WithContext<FAQPage> = {
24
+ '@context': 'https://schema.org',
25
+ '@type': 'FAQPage',
26
+ mainEntity: faqData.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })),
27
+ };
28
+
29
+ const howToSchema: WithContext<HowToThing> = {
30
+ '@context': 'https://schema.org',
31
+ '@type': 'HowTo',
32
+ name: title,
33
+ description,
34
+ step: howToData.map((step, i) => ({ '@type': 'HowToStep', position: i + 1, name: step.name, text: step.text })),
35
+ };
36
+
37
+ const appSchema: WithContext<SoftwareApplication> = {
38
+ '@context': 'https://schema.org',
39
+ '@type': 'SoftwareApplication',
40
+ name: title,
41
+ description,
42
+ applicationCategory: 'DesignApplication',
43
+ operatingSystem: 'All',
44
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
45
+ inLanguage: 'fr',
46
+ };
47
+
48
+ const ui: MobileMockupGeneratorUI = {
49
+ labelUpload: 'Télécharger des captures',
50
+ dropHint: 'Glissez vos images ici',
51
+ dropFormats: 'PNG, JPG ou WEBP',
52
+ btnMassReplace: 'Remplacement en masse',
53
+ massReplaceHint: 'Remplace les images actuelles en conservant vos presets et textes. Idéal pour les itérations rapides.',
54
+ labelSettings: 'Paramètres globaux',
55
+ labelDevice: 'Appareil',
56
+ labelFont: 'Typographie',
57
+ labelBackground: 'Fond',
58
+ titleSwapColors: 'Inverser les couleurs',
59
+ labelAngle: 'Angle',
60
+ labelSafeArea: 'Zone de sécurité (haut/bas)',
61
+ labelSafeAreaColor: 'Couleur de zone',
62
+ emptyTitle: 'Aucune image pour l\'instant',
63
+ emptySubtitle: 'Téléchargez vos captures pour commencer à concevoir',
64
+ btnClearAll: 'Effacer le canevas',
65
+ btnExport: 'Tout exporter (.zip)',
66
+ cardTitleDuplicate: 'Dupliquer la scène',
67
+ cardTitleReplace: 'Remplacer la capture actuelle',
68
+ cardSectionLayouts: 'Mises en page maîtres',
69
+ cardSectionBranding: 'Marque & Textes',
70
+ cardTitleResetText: 'Réinitialiser le texte',
71
+ cardLabelColor: 'Couleur',
72
+ cardTextPlaceholder: 'Écrivez votre texte ici...',
73
+ cardSectionDevice: 'Mise en page appareil',
74
+ cardTitleResetDevice: 'Réinitialiser la position',
75
+ cardSectionScene: 'Accessoires & Fond',
76
+ cardBtnSpecialBg: 'Fond spécial',
77
+ cardBtnDeleteScene: 'Supprimer la scène',
78
+ cardRangeLabelSize: 'Taille',
79
+ cardRangeLabelX: 'Axe X',
80
+ cardRangeLabelY: 'Axe Y',
81
+ cardRangeLabelRotation: 'Rotation',
82
+ cardRangeLabelScale: 'Échelle',
83
+ presetClassic: 'Classique',
84
+ presetMobileBottom: 'Mobile bas',
85
+ presetMobileTop: 'Mobile haut',
86
+ presetFocus: 'Focus',
87
+ presetDynamic: 'Dynamique',
88
+ presetSplitLeft: 'Divisé gauche',
89
+ presetSplitRight: 'Divisé droite',
90
+ presetImageLeft: 'Image gauche',
91
+ presetImageRight: 'Image droite',
92
+ confirmClear: 'Êtes-vous sûr de vouloir supprimer tous les mockups ?',
93
+ processingExport: 'Traitement en cours...',
94
+ exportDone: 'Terminé !',
95
+ };
96
+
97
+ export const content: ToolLocaleContent<MobileMockupGeneratorUI> = {
98
+ slug,
99
+ title,
100
+ description,
101
+ ui,
102
+ faqTitle: 'Questions fréquentes',
103
+ faq: faqData,
104
+ bibliographyTitle: 'Références',
105
+ bibliography: [
106
+ { name: 'Apple App Store Screenshot Requirements', url: 'https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications' },
107
+ { name: 'Google Play Screenshot Requirements', url: 'https://support.google.com/googleplay/android-developer/answer/9866151' },
108
+ ],
109
+ howTo: howToData,
110
+ schemas: [faqSchema, howToSchema, appSchema],
111
+ seo: [
112
+ { type: 'title', text: 'Élevez vos captures au niveau supérieur', level: 2 },
113
+ { type: 'paragraph', html: 'Ne vous limitez pas à des captures d\'écran plates. Notre outil de mockups permet aux développeurs et designers de créer des visuels à fort impact sans avoir besoin de Photoshop ou Figma.' },
114
+ { type: 'title', text: 'Puissance avec les mises en page maîtres', level: 3 },
115
+ { type: 'grid', columns: 2 },
116
+ { type: 'card', title: 'App Store et Google Play', html: '<p>Optimisez votre taux de conversion. Les mockups iPhone 15 Pro Max et Pixel 8 sont le standard mondial pour les fiches des boutiques d\'applications.</p>' },
117
+ { type: 'card', title: 'Pitch Decks et Marketing', html: '<p>Présentez vos idées avec autorité. Parfait pour les présentations aux investisseurs, les campagnes sur les réseaux sociaux et les portfolios professionnels de design UI/UX.</p>' },
118
+ { type: 'title', text: 'Flux de travail professionnel', level: 3 },
119
+ { type: 'tip', html: 'Utilisez les presets "Divisé gauche" et "Divisé droite" pour créer des mockups continus qui glissent d\'une image à l\'autre dans les carrousels Instagram ou les captures App Store.' },
120
+ ],
121
+ };
@@ -0,0 +1,33 @@
1
+ import type { DeveloperToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import MobileMockupGeneratorComponent from './component.astro';
3
+ import MobileMockupGeneratorSEO from './seo.astro';
4
+ import MobileMockupGeneratorBibliography from './bibliography.astro';
5
+ import type { MobileMockupGeneratorUI } from './ui';
6
+
7
+ export type MobileMockupGeneratorLocaleContent = ToolLocaleContent<MobileMockupGeneratorUI>;
8
+
9
+ import { content as es } from './i18n/es';
10
+ import { content as en } from './i18n/en';
11
+ import { content as fr } from './i18n/fr';
12
+
13
+ export const mobileMockupGenerator: DeveloperToolEntry<MobileMockupGeneratorUI> = {
14
+ id: 'mobile-mockup-generator',
15
+ icons: {
16
+ bg: 'mdi:cellphone',
17
+ fg: 'mdi:image-frame',
18
+ },
19
+ i18n: {
20
+ es: async () => es,
21
+ en: async () => en,
22
+ fr: async () => fr,
23
+ },
24
+ };
25
+
26
+ export { MobileMockupGeneratorComponent, MobileMockupGeneratorSEO, MobileMockupGeneratorBibliography };
27
+
28
+ export const MOBILE_MOCKUP_GENERATOR_TOOL: ToolDefinition = {
29
+ entry: mobileMockupGenerator,
30
+ Component: MobileMockupGeneratorComponent,
31
+ SEOComponent: MobileMockupGeneratorSEO,
32
+ BibliographyComponent: MobileMockupGeneratorBibliography,
33
+ };
@@ -0,0 +1,189 @@
1
+ import type { DeviceConfig, MockupImage } from './types';
2
+ import { EXPORT_WIDTH, EXPORT_HEIGHT, CANVAS_ASPECT } from './constants';
3
+
4
+ interface CanvasDims { w: number; h: number; }
5
+ interface Rect { x: number; y: number; w: number; h: number; }
6
+ interface SafeAreaBounds { targetY: number; targetH: number; }
7
+
8
+ interface GlobalSettings {
9
+ gradient: { start: string; end: string; angle: number };
10
+ font: string;
11
+ safeArea: { show: boolean; color: string };
12
+ }
13
+
14
+ interface DrawState {
15
+ ctx: CanvasRenderingContext2D;
16
+ image: MockupImage;
17
+ gs: GlobalSettings;
18
+ scale: number;
19
+ }
20
+
21
+ export interface DrawMockupOpts {
22
+ dataUrl: string;
23
+ device: DeviceConfig;
24
+ gs: GlobalSettings;
25
+ isExport?: boolean;
26
+ }
27
+
28
+ export class RenderingEngine {
29
+ static async drawMockup(canvas: HTMLCanvasElement, image: MockupImage, opts: DrawMockupOpts) {
30
+ const ctx = canvas.getContext('2d', { alpha: false });
31
+ if (!ctx) return;
32
+ const dims = this.initCanvas(canvas, ctx, opts.isExport ?? false);
33
+ const { scale, devBounds } = this.calcDeviceLayout(image, opts.device, dims);
34
+ const s: DrawState = { ctx, image, gs: opts.gs, scale };
35
+ await this.drawBackground(s, dims);
36
+ this.drawDeviceShadow(s, devBounds, opts.device.radius);
37
+ const screen = this.getScreenBounds(devBounds, scale);
38
+ await this.drawScreenContent(s, opts.dataUrl, opts.device, screen);
39
+ if (opts.device.notch) this.drawNotch(s, opts.device, devBounds);
40
+ this.drawText(s, dims);
41
+ }
42
+
43
+ private static initCanvas(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, isExport: boolean): CanvasDims {
44
+ if (isExport) return { w: EXPORT_WIDTH, h: EXPORT_HEIGHT };
45
+ const dpr = window.devicePixelRatio || 1;
46
+ const w = canvas.clientWidth || canvas.width / dpr;
47
+ const h = w / CANVAS_ASPECT;
48
+ canvas.width = w * dpr;
49
+ canvas.height = h * dpr;
50
+ ctx.scale(dpr, dpr);
51
+ return { w, h };
52
+ }
53
+
54
+ private static async drawBackground(s: DrawState, dims: CanvasDims) {
55
+ if (s.image.settings.bgImage) {
56
+ const bgImg = await this.loadImage(s.image.settings.bgImage);
57
+ s.ctx.drawImage(bgImg, 0, 0, dims.w, dims.h);
58
+ return;
59
+ }
60
+ const angleRad = (s.gs.gradient.angle - 90) * (Math.PI / 180);
61
+ const cx = dims.w / 2;
62
+ const cy = dims.h / 2;
63
+ const len = Math.abs(dims.w * Math.cos(angleRad)) + Math.abs(dims.h * Math.sin(angleRad));
64
+ const grad = s.ctx.createLinearGradient(cx - Math.cos(angleRad) * len / 2, cy - Math.sin(angleRad) * len / 2, cx + Math.cos(angleRad) * len / 2, cy + Math.sin(angleRad) * len / 2);
65
+ grad.addColorStop(0, s.gs.gradient.start);
66
+ grad.addColorStop(1, s.gs.gradient.end);
67
+ s.ctx.fillStyle = grad;
68
+ s.ctx.fillRect(0, 0, dims.w, dims.h);
69
+ }
70
+
71
+ private static calcDeviceLayout(image: MockupImage, device: DeviceConfig, dims: CanvasDims) {
72
+ const paddingScale = 0.85 - (image.settings.spacing / 100) * 0.35;
73
+ const scale = Math.min((dims.w * paddingScale) / device.width, (dims.h * paddingScale) / device.height);
74
+ const dW = device.width * scale;
75
+ const dH = device.height * scale;
76
+ const dX = (dims.w - dW) / 2 + (dims.w * (image.settings.deviceOffsetX ?? 0)) / 100;
77
+ const dY = (dims.h - dH) / 2 + (dims.h * (image.settings.deviceOffsetY ?? 0)) / 100;
78
+ return { scale, devBounds: { x: dX, y: dY, w: dW, h: dH } };
79
+ }
80
+
81
+ private static drawDeviceShadow(s: DrawState, devBounds: Rect, radius: number) {
82
+ s.ctx.save();
83
+ s.ctx.shadowColor = 'rgba(0,0,0,0.4)';
84
+ s.ctx.shadowBlur = 40 * s.scale;
85
+ s.ctx.shadowOffsetY = 20 * s.scale;
86
+ s.ctx.fillStyle = '#0f172a';
87
+ this.roundRect(s.ctx, devBounds, radius * s.scale);
88
+ s.ctx.fill();
89
+ s.ctx.restore();
90
+ }
91
+
92
+ private static getScreenBounds(devBounds: Rect, scale: number): Rect {
93
+ const inset = 8 * scale;
94
+ return { x: devBounds.x + inset, y: devBounds.y + inset, w: devBounds.w - inset * 2, h: devBounds.h - inset * 2 };
95
+ }
96
+
97
+ private static async drawScreenContent(s: DrawState, dataUrl: string, device: DeviceConfig, screen: Rect) {
98
+ const imgEl = await this.loadImage(dataUrl);
99
+ s.ctx.save();
100
+ this.roundRect(s.ctx, screen, (device.radius - 8) * s.scale);
101
+ s.ctx.clip();
102
+ if (s.gs.safeArea.show) {
103
+ s.ctx.fillStyle = s.gs.safeArea.color;
104
+ s.ctx.fillRect(screen.x, screen.y, screen.w, screen.h);
105
+ }
106
+ const saBounds = this.calcSafeAreaBounds(device, screen, s.gs.safeArea.show, s.scale);
107
+ const dr = this.calcImageDrawRect(imgEl, screen, saBounds, s.gs.safeArea.show);
108
+ s.ctx.drawImage(imgEl, dr.x, dr.y, dr.w, dr.h);
109
+ if (s.gs.safeArea.show) this.drawSafeAreaOverlay(s.ctx, device, screen, { scale: s.scale, color: s.gs.safeArea.color });
110
+ s.ctx.restore();
111
+ }
112
+
113
+ private static calcSafeAreaBounds(device: DeviceConfig, screen: Rect, show: boolean, scale: number): SafeAreaBounds {
114
+ if (!show) return { targetY: screen.y, targetH: screen.h };
115
+ return {
116
+ targetY: screen.y + device.safeAreaTop * scale,
117
+ targetH: screen.h - (device.safeAreaTop + device.safeAreaBottom) * scale,
118
+ };
119
+ }
120
+
121
+ private static calcImageDrawRect(img: HTMLImageElement, screen: Rect, bounds: SafeAreaBounds, hasSafeArea: boolean): Rect {
122
+ const iA = img.width / img.height;
123
+ const sA = screen.w / bounds.targetH;
124
+ if (hasSafeArea) {
125
+ if (iA > sA) return { x: screen.x, y: bounds.targetY + (bounds.targetH - screen.w / iA) / 2, w: screen.w, h: screen.w / iA };
126
+ return { x: screen.x + (screen.w - bounds.targetH * iA) / 2, y: bounds.targetY, w: bounds.targetH * iA, h: bounds.targetH };
127
+ }
128
+ if (iA > sA) return { x: screen.x - (screen.h * iA - screen.w) / 2, y: screen.y, w: screen.h * iA, h: screen.h };
129
+ return { x: screen.x, y: screen.y - (screen.w / iA - screen.h) / 2, w: screen.w, h: screen.w / iA };
130
+ }
131
+
132
+ private static drawSafeAreaOverlay(ctx: CanvasRenderingContext2D, device: DeviceConfig, screen: Rect, opts: { scale: number; color: string }) {
133
+ ctx.fillStyle = opts.color;
134
+ if (device.safeAreaTop > 0) ctx.fillRect(screen.x, screen.y, screen.w, device.safeAreaTop * opts.scale);
135
+ if (device.safeAreaBottom > 0) ctx.fillRect(screen.x, screen.y + screen.h - device.safeAreaBottom * opts.scale, screen.w, device.safeAreaBottom * opts.scale);
136
+ }
137
+
138
+ private static drawNotch(s: DrawState, device: DeviceConfig, devBounds: Rect) {
139
+ if (!device.notch) return;
140
+ s.ctx.fillStyle = '#0f172a';
141
+ const nW = 120 * s.scale;
142
+ const nH = 35 * s.scale;
143
+ const nR = 18 * s.scale;
144
+ this.roundRect(s.ctx, { x: devBounds.x + (devBounds.w - nW) / 2, y: devBounds.y + 12 * s.scale, w: nW, h: nH }, nR);
145
+ s.ctx.fill();
146
+ }
147
+
148
+ private static drawText(s: DrawState, dims: CanvasDims) {
149
+ if (!s.image.settings.text) return;
150
+ s.ctx.save();
151
+ s.ctx.translate(dims.w * (s.image.settings.textX / 100), dims.h * (s.image.settings.textY / 100));
152
+ s.ctx.rotate((s.image.settings.textRotation * Math.PI) / 180);
153
+ s.ctx.font = `bold ${s.image.settings.textSize}px ${s.gs.font}`;
154
+ s.ctx.fillStyle = s.image.settings.textColor;
155
+ s.ctx.textAlign = 'center';
156
+ s.ctx.textBaseline = 'middle';
157
+ const lines = this.wrapText(s.ctx, s.image.settings.text, dims.w * 0.85);
158
+ const lineH = s.image.settings.textSize * 1.2;
159
+ lines.forEach((line, i) => s.ctx.fillText(line, 0, i * lineH - ((lines.length - 1) * lineH) / 2));
160
+ s.ctx.restore();
161
+ }
162
+
163
+ private static wrapText(ctx: CanvasRenderingContext2D, text: string, maxW: number): string[] {
164
+ const words = text.split(' ');
165
+ const lines: string[] = [];
166
+ let current = words[0];
167
+ for (let i = 1; i < words.length; i++) {
168
+ const test = current + ' ' + words[i];
169
+ if (ctx.measureText(test).width < maxW) { current = test; }
170
+ else { lines.push(current); current = words[i]; }
171
+ }
172
+ lines.push(current);
173
+ return lines;
174
+ }
175
+
176
+ private static roundRect(ctx: CanvasRenderingContext2D, bounds: Rect, r: number) {
177
+ ctx.beginPath();
178
+ ctx.roundRect(bounds.x, bounds.y, bounds.w, bounds.h, r);
179
+ ctx.closePath();
180
+ }
181
+
182
+ private static loadImage(url: string): Promise<HTMLImageElement> {
183
+ return new Promise((resolve) => {
184
+ const img = new Image();
185
+ img.onload = () => resolve(img);
186
+ img.src = url;
187
+ });
188
+ }
189
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } 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 && <SEORenderer content={{ locale, sections: content.seo }} />}