@jjlmoya/utils-nautical 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 (74) hide show
  1. package/package.json +75 -0
  2. package/src/category/i18n/en.ts +79 -0
  3. package/src/category/i18n/es.ts +79 -0
  4. package/src/category/i18n/fr.ts +79 -0
  5. package/src/category/index.ts +13 -0
  6. package/src/components/PreviewNavSidebar.astro +116 -0
  7. package/src/components/PreviewToolbar.astro +143 -0
  8. package/src/data.ts +12 -0
  9. package/src/env.d.ts +5 -0
  10. package/src/index.ts +22 -0
  11. package/src/layouts/PreviewLayout.astro +117 -0
  12. package/src/pages/[locale]/[slug].astro +146 -0
  13. package/src/pages/[locale].astro +249 -0
  14. package/src/pages/index.astro +4 -0
  15. package/src/tests/faq_count.test.ts +30 -0
  16. package/src/tests/mocks/astro_mock.js +2 -0
  17. package/src/tests/seo_length.test.ts +29 -0
  18. package/src/tests/tool_validation.test.ts +51 -0
  19. package/src/tool/endurance/bibliography.astro +14 -0
  20. package/src/tool/endurance/component.astro +310 -0
  21. package/src/tool/endurance/i18n/en.ts +243 -0
  22. package/src/tool/endurance/i18n/es.ts +249 -0
  23. package/src/tool/endurance/i18n/fr.ts +217 -0
  24. package/src/tool/endurance/index.ts +56 -0
  25. package/src/tool/endurance/logic.ts +36 -0
  26. package/src/tool/endurance/seo.astro +55 -0
  27. package/src/tool/endurance/style.css +333 -0
  28. package/src/tool/nauticalConverter/bibliography.astro +14 -0
  29. package/src/tool/nauticalConverter/component.astro +481 -0
  30. package/src/tool/nauticalConverter/i18n/en.ts +239 -0
  31. package/src/tool/nauticalConverter/i18n/es.ts +239 -0
  32. package/src/tool/nauticalConverter/i18n/fr.ts +239 -0
  33. package/src/tool/nauticalConverter/index.ts +57 -0
  34. package/src/tool/nauticalConverter/logic.ts +52 -0
  35. package/src/tool/nauticalConverter/seo.astro +52 -0
  36. package/src/tool/nauticalConverter/style.css +205 -0
  37. package/src/tool/sailArea/bibliography.astro +14 -0
  38. package/src/tool/sailArea/component.astro +418 -0
  39. package/src/tool/sailArea/i18n/en.ts +275 -0
  40. package/src/tool/sailArea/i18n/es.ts +275 -0
  41. package/src/tool/sailArea/i18n/fr.ts +275 -0
  42. package/src/tool/sailArea/index.ts +53 -0
  43. package/src/tool/sailArea/logic.ts +55 -0
  44. package/src/tool/sailArea/seo.astro +52 -0
  45. package/src/tool/sailArea/style.css +351 -0
  46. package/src/tool/speedConverter/bibliography.astro +14 -0
  47. package/src/tool/speedConverter/component.astro +125 -0
  48. package/src/tool/speedConverter/i18n/en.ts +271 -0
  49. package/src/tool/speedConverter/i18n/es.ts +271 -0
  50. package/src/tool/speedConverter/i18n/fr.ts +271 -0
  51. package/src/tool/speedConverter/index.ts +46 -0
  52. package/src/tool/speedConverter/logic.ts +48 -0
  53. package/src/tool/speedConverter/seo.astro +43 -0
  54. package/src/tool/speedConverter/style.css +239 -0
  55. package/src/tool/tideCalculator/bibliography.astro +14 -0
  56. package/src/tool/tideCalculator/component.astro +314 -0
  57. package/src/tool/tideCalculator/i18n/en.ts +203 -0
  58. package/src/tool/tideCalculator/i18n/es.ts +225 -0
  59. package/src/tool/tideCalculator/i18n/fr.ts +190 -0
  60. package/src/tool/tideCalculator/index.ts +50 -0
  61. package/src/tool/tideCalculator/logic.ts +73 -0
  62. package/src/tool/tideCalculator/seo.astro +61 -0
  63. package/src/tool/tideCalculator/style.css +310 -0
  64. package/src/tool/underKeel/bibliography.astro +14 -0
  65. package/src/tool/underKeel/component.astro +240 -0
  66. package/src/tool/underKeel/i18n/en.ts +193 -0
  67. package/src/tool/underKeel/i18n/es.ts +206 -0
  68. package/src/tool/underKeel/i18n/fr.ts +193 -0
  69. package/src/tool/underKeel/index.ts +49 -0
  70. package/src/tool/underKeel/logic.ts +83 -0
  71. package/src/tool/underKeel/seo.astro +46 -0
  72. package/src/tool/underKeel/style.css +312 -0
  73. package/src/tools.ts +25 -0
  74. package/src/types.ts +69 -0
@@ -0,0 +1,418 @@
1
+ ---
2
+ import './style.css';
3
+ import type { SailAreaUI } from './index';
4
+
5
+ interface Props {
6
+ ui: SailAreaUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <div id="sail-area-calculator-root" class="sail-area-calculator" data-ui={JSON.stringify(ui)}>
13
+ <div class="sa-calculator-wrapper" id="calculator-capture-area">
14
+ <div class="sa-main-controls">
15
+
16
+ <div class="sa-section-group">
17
+ <h3>{ui.hullSectionLabel}</h3>
18
+ <div class="sa-compact-grid">
19
+ <div class="sa-input-row">
20
+ <label for="displacement">{ui.totalMassLabel}</label>
21
+ <div class="sa-input-with-unit">
22
+ <input type="number" id="displacement" class="sa-elegant-input" placeholder="8500" />
23
+ <div class="sa-unit-tag">
24
+ <select id="disp-unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
25
+ <option value="kg">KG</option>
26
+ <option value="lbs">LBS</option>
27
+ </select>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ <div class="sa-input-row">
32
+ <label for="rig-type">{ui.sailConfigLabel}</label>
33
+ <select id="rig-type" class="sa-elegant-input">
34
+ <option value="sloop">{ui.sloopLabel}</option>
35
+ <option value="cutter">{ui.cutterLabel}</option>
36
+ <option value="ketch">{ui.ketchLabel}</option>
37
+ </select>
38
+ </div>
39
+ <div class="sa-input-row">
40
+ <label for="boat-type">{ui.intendedUseLabel}</label>
41
+ <select id="boat-type" class="sa-elegant-input">
42
+ <option value="cruiser">{ui.cruiserLabel}</option>
43
+ <option value="performance">{ui.performanceTypeLabel}</option>
44
+ <option value="racer">{ui.racerLabel}</option>
45
+ </select>
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="sa-section-group">
51
+ <h3>{ui.rigGeometryLabel}</h3>
52
+ <div class="sa-compact-grid">
53
+ <div class="sa-input-row">
54
+ <label>P (m)</label>
55
+ <div class="sa-input-with-unit">
56
+ <input type="number" id="val-p" class="sa-elegant-input" placeholder="12.5" />
57
+ <div class="sa-unit-tag">
58
+ <select class="sa-len-unit" data-for="val-p" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
59
+ <option value="m">M</option>
60
+ <option value="ft">FT</option>
61
+ </select>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <div class="sa-input-row">
66
+ <label>E (m)</label>
67
+ <div class="sa-input-with-unit">
68
+ <input type="number" id="val-e" class="sa-elegant-input" placeholder="4.2" />
69
+ <div class="sa-unit-tag">
70
+ <select class="sa-len-unit" data-for="val-e" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
71
+ <option value="m">M</option>
72
+ <option value="ft">FT</option>
73
+ </select>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ <div class="sa-input-row">
78
+ <label>I (m)</label>
79
+ <div class="sa-input-with-unit">
80
+ <input type="number" id="val-i" class="sa-elegant-input" placeholder="14.0" />
81
+ <div class="sa-unit-tag">
82
+ <select class="sa-len-unit" data-for="val-i" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
83
+ <option value="m">M</option>
84
+ <option value="ft">FT</option>
85
+ </select>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <div class="sa-input-row">
90
+ <label>J (m)</label>
91
+ <div class="sa-input-with-unit">
92
+ <input type="number" id="val-j" class="sa-elegant-input" placeholder="4.5" />
93
+ <div class="sa-unit-tag">
94
+ <select class="sa-len-unit" data-for="val-j" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
95
+ <option value="m">M</option>
96
+ <option value="ft">FT</option>
97
+ </select>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <div id="mizzen-row" style="display: none; margin-top: 1.5rem;" class="sa-compact-grid">
104
+ <div class="sa-input-row">
105
+ <label>Py (m)</label>
106
+ <div class="sa-input-with-unit">
107
+ <input type="number" id="val-py" class="sa-elegant-input" placeholder="8.5" />
108
+ <div class="sa-unit-tag">
109
+ <select class="sa-len-unit" data-for="val-py" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
110
+ <option value="m">M</option>
111
+ <option value="ft">FT</option>
112
+ </select>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ <div class="sa-input-row">
117
+ <label>Ey (m)</label>
118
+ <div class="sa-input-with-unit">
119
+ <input type="number" id="val-ey" class="sa-elegant-input" placeholder="3.0" />
120
+ <div class="sa-unit-tag">
121
+ <select class="sa-len-unit" data-for="val-ey" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
122
+ <option value="m">M</option>
123
+ <option value="ft">FT</option>
124
+ </select>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="sa-section-group">
132
+ <h3>{ui.genoaOverlapLabel}</h3>
133
+ <div class="sa-slider-box">
134
+ <div class="sa-slider-header">
135
+ <label for="genoa-overlap">{ui.genoaOverlapLabel} (LP/J)</label>
136
+ <span id="overlap-val" class="sa-percentage-badge">130%</span>
137
+ </div>
138
+ <div class="sa-slider-wrapper">
139
+ <input type="range" id="genoa-overlap" class="sa-modern-slider" min="100" max="155" value="130" step="1" />
140
+ <div class="sa-slider-ticks">
141
+ <span>100% ({ui.foqueLabel})</span>
142
+ <span>155% ({ui.genovaLabel} G.)</span>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ </div>
149
+
150
+ <aside class="sa-sidebar-results">
151
+ <span class="sad-label">{ui.sadRatioLabel}</span>
152
+ <span id="sad-ratio-val" class="sad-value-large">0.0</span>
153
+ <div class="sa-gauge-track">
154
+ <div id="gauge-marker-el" class="sa-gauge-indicator" style="left: 0%"></div>
155
+ </div>
156
+ <div class="sa-verdict-box">
157
+ <h4 id="verdict-title-el">-</h4>
158
+ <p id="verdict-text-el">-</p>
159
+ </div>
160
+ <div class="sa-stats-summary">
161
+ <div class="sa-stat-item">
162
+ <span>{ui.totalAreaLabel}</span>
163
+ <span id="total-area-el">0.0 m²</span>
164
+ </div>
165
+ <div class="sa-stat-item">
166
+ <span>{ui.performanceLabel}</span>
167
+ <span id="power-indicator-el">-</span>
168
+ </div>
169
+ </div>
170
+ <button id="btn-export-pdf-premium" class="sa-export-btn-premium sa-ignore-capture">
171
+ {ui.exportPdfLabel}
172
+ </button>
173
+ </aside>
174
+ </div>
175
+ </div>
176
+
177
+ <script>
178
+ const root = document.getElementById('sail-area-calculator-root') as HTMLElement;
179
+ const ui = JSON.parse(root.dataset.ui as string) as Record<string, string>;
180
+
181
+ const rigType = document.getElementById('rig-type') as HTMLSelectElement;
182
+ const displacement = document.getElementById('displacement') as HTMLInputElement;
183
+ const dispUnit = document.getElementById('disp-unit') as HTMLSelectElement;
184
+ const valP = document.getElementById('val-p') as HTMLInputElement;
185
+ const valE = document.getElementById('val-e') as HTMLInputElement;
186
+ const valI = document.getElementById('val-i') as HTMLInputElement;
187
+ const valJ = document.getElementById('val-j') as HTMLInputElement;
188
+ const valPy = document.getElementById('val-py') as HTMLInputElement;
189
+ const valEy = document.getElementById('val-ey') as HTMLInputElement;
190
+ const genoaOverlap = document.getElementById('genoa-overlap') as HTMLInputElement;
191
+ const overlapLabel = document.getElementById('overlap-val') as HTMLElement;
192
+ const totalAreaDisplay = document.getElementById('total-area-el') as HTMLElement;
193
+ const sadRatioDisplay = document.getElementById('sad-ratio-val') as HTMLElement;
194
+ const verdictTitle = document.getElementById('verdict-title-el') as HTMLElement;
195
+ const verdictText = document.getElementById('verdict-text-el') as HTMLElement;
196
+ const gaugeMarker = document.getElementById('gauge-marker-el') as HTMLElement;
197
+ const powerIndicator = document.getElementById('power-indicator-el') as HTMLElement;
198
+ const mizzenRow = document.getElementById('mizzen-row') as HTMLElement;
199
+ const lenUnits = document.querySelectorAll<HTMLSelectElement>('.sa-len-unit');
200
+
201
+ function getValInM(id: string): number {
202
+ const el = document.getElementById(id) as HTMLInputElement;
203
+ const unitEl = document.querySelector<HTMLSelectElement>(`.sa-len-unit[data-for="${id}"]`);
204
+ const val = parseFloat(el.value) || 0;
205
+ return unitEl?.value === 'ft' ? val * 0.3048 : val;
206
+ }
207
+
208
+ function calculate() {
209
+ const p = getValInM('val-p');
210
+ const e = getValInM('val-e');
211
+ const i = getValInM('val-i');
212
+ const j = getValInM('val-j');
213
+ const py = getValInM('val-py');
214
+ const ey = getValInM('val-ey');
215
+ const overlap = (parseFloat(genoaOverlap.value) || 100) / 100;
216
+ let dispKg = parseFloat(displacement.value) || 0;
217
+ if (dispUnit.value === 'lbs') dispKg *= 0.453592;
218
+
219
+ const mainA = 0.5 * p * e;
220
+ const genoaA = 0.5 * i * j * overlap;
221
+ let totalA = mainA + genoaA;
222
+ if (rigType.value === 'ketch') totalA += 0.5 * py * ey;
223
+ if (rigType.value === 'cutter') totalA += 0.5 * i * j * 0.4;
224
+
225
+ totalAreaDisplay.textContent = `${totalA.toFixed(1)} m²`;
226
+ const areaSqFt = totalA * 10.7639;
227
+ const dispLbs = dispKg * 2.20462;
228
+ if (dispLbs > 100 && areaSqFt > 1) {
229
+ const sad = Math.min(99.9, areaSqFt / Math.pow(dispLbs / 64, 2 / 3));
230
+ sadRatioDisplay.textContent = sad.toFixed(1);
231
+ updateUI(sad);
232
+ }
233
+ }
234
+
235
+ function updateUI(sad: number) {
236
+ let title = ui.cruiserLabel ?? 'Crucero';
237
+ let text = 'Diseno robusto ideal para seguridad oceanica y travesias.';
238
+ let power = 'Moderada';
239
+ const gaugePos = Math.min(100, Math.max(0, (sad - 10) * 4));
240
+ if (sad > 16.5) {
241
+ title = ui.cruiserLabel ?? 'Crucero';
242
+ text = 'Equilibrio ideal para la mayoria de navegantes de recreo.';
243
+ power = 'Optima';
244
+ }
245
+ if (sad > 20) {
246
+ title = ui.performanceTypeLabel ?? 'Performance';
247
+ text = 'Barcos rapidos que requieren manejo activo de escotas.';
248
+ power = 'Alta';
249
+ }
250
+ if (sad > 25) {
251
+ title = ui.racerLabel ?? 'Regata';
252
+ text = 'Maxima agilidad. Respuesta inmediata y potencial de planeo.';
253
+ power = 'Extrema';
254
+ }
255
+ verdictTitle.textContent = title;
256
+ verdictText.textContent = text;
257
+ powerIndicator.textContent = power;
258
+ gaugeMarker.style.left = `${gaugePos}%`;
259
+ }
260
+
261
+ function saveToLocal() {
262
+ const data = {
263
+ rig: rigType.value,
264
+ disp: displacement.value,
265
+ dispU: dispUnit.value,
266
+ p: valP.value,
267
+ e: valE.value,
268
+ i: valI.value,
269
+ j: valJ.value,
270
+ py: valPy.value,
271
+ ey: valEy.value,
272
+ overlap: genoaOverlap.value,
273
+ lenUnits: Array.from(lenUnits).map((u) => ({ id: u.getAttribute('data-for'), val: u.value })),
274
+ };
275
+ localStorage.setItem('sail_calc_data', JSON.stringify(data));
276
+ }
277
+
278
+ type SavedData = { rig?: string; disp?: string; dispU?: string; p?: string; e?: string; i?: string; j?: string; py?: string; ey?: string; overlap?: string; lenUnits?: { id: string; val: string }[] };
279
+
280
+ function applyDimensions(d: SavedData) {
281
+ valP.value = d.p || '';
282
+ valE.value = d.e || '';
283
+ valI.value = d.i || '';
284
+ valJ.value = d.j || '';
285
+ valPy.value = d.py || '';
286
+ valEy.value = d.ey || '';
287
+ }
288
+
289
+ function applyMeta(d: SavedData) {
290
+ genoaOverlap.value = d.overlap || '130';
291
+ overlapLabel.textContent = `${genoaOverlap.value}%`;
292
+ (d.lenUnits || []).forEach((u) => {
293
+ const el = document.querySelector<HTMLSelectElement>(`.sa-len-unit[data-for="${u.id}"]`);
294
+ if (el) el.value = u.val;
295
+ });
296
+ mizzenRow.style.display = d.rig === 'ketch' ? 'grid' : 'none';
297
+ }
298
+
299
+ function loadFromLocal() {
300
+ const raw = localStorage.getItem('sail_calc_data');
301
+ if (!raw) { calculate(); return; }
302
+ try {
303
+ const data = JSON.parse(raw) as SavedData;
304
+ rigType.value = data.rig || 'sloop';
305
+ displacement.value = data.disp || '';
306
+ dispUnit.value = data.dispU || 'kg';
307
+ applyDimensions(data);
308
+ applyMeta(data);
309
+ calculate();
310
+ } catch { calculate(); }
311
+ }
312
+
313
+ [rigType, displacement, dispUnit, valP, valE, valI, valJ, valPy, valEy, genoaOverlap].forEach((el) => {
314
+ el.addEventListener('input', () => {
315
+ mizzenRow.style.display = rigType.value === 'ketch' ? 'grid' : 'none';
316
+ overlapLabel.textContent = `${genoaOverlap.value}%`;
317
+ calculate();
318
+ saveToLocal();
319
+ });
320
+ });
321
+
322
+ lenUnits.forEach((u) => u.addEventListener('change', () => { calculate(); saveToLocal(); }));
323
+
324
+ function getLenUnit(id: string): string {
325
+ return document.querySelector<HTMLSelectElement>(`.sa-len-unit[data-for="${id}"]`)?.value ?? 'm';
326
+ }
327
+
328
+ function buildMizzenRows(): string {
329
+ if (rigType.value !== 'ketch') return '';
330
+ const td = 'style="padding:10px;border:1px solid #ddd;font-size:13px;"';
331
+ const tdB = 'style="padding:10px;border:1px solid #ddd;font-size:13px;font-weight:bold;"';
332
+ return `<tr><td ${td}>Py (Altura Mesana)</td><td ${tdB}>${valPy.value} ${getLenUnit('val-py')}</td></tr>
333
+ <tr><td ${td}>Ey (Base Mesana)</td><td ${tdB}>${valEy.value} ${getLenUnit('val-ey')}</td></tr>`;
334
+ }
335
+
336
+ function buildPdfTable(): string {
337
+ const td = 'style="padding:10px;border:1px solid #ddd;font-size:13px;"';
338
+ const tdB = 'style="padding:10px;border:1px solid #ddd;font-size:13px;font-weight:bold;"';
339
+ const th = 'style="background:#003b73;color:#fff;"';
340
+ const thCell = 'style="padding:10px;text-align:left;font-size:12px;border:1px solid #003b73;"';
341
+ return `<table style="width:100%;border-collapse:collapse;margin-bottom:30px;">
342
+ <thead><tr ${th}><th ${thCell}>Parametro</th><th ${thCell}>Valor</th></tr></thead>
343
+ <tbody>
344
+ <tr><td ${td}>Tipo de Aparejo</td><td ${tdB}>${rigType.options[rigType.selectedIndex]?.text ?? ''}</td></tr>
345
+ <tr><td ${td}>Desplazamiento</td><td ${tdB}>${displacement.value} ${dispUnit.value}</td></tr>
346
+ <tr><td ${td}>P (Mastil Mayor)</td><td ${tdB}>${valP.value} ${getLenUnit('val-p')}</td></tr>
347
+ <tr><td ${td}>E (Botavara Mayor)</td><td ${tdB}>${valE.value} ${getLenUnit('val-e')}</td></tr>
348
+ <tr><td ${td}>I (Altura Foretriangulo)</td><td ${tdB}>${valI.value} ${getLenUnit('val-i')}</td></tr>
349
+ <tr><td ${td}>J (Base Foretriangulo)</td><td ${tdB}>${valJ.value} ${getLenUnit('val-j')}</td></tr>
350
+ <tr><td ${td}>Solape Genova</td><td ${tdB}>${genoaOverlap.value}%</td></tr>
351
+ ${buildMizzenRows()}
352
+ </tbody>
353
+ </table>`;
354
+ }
355
+
356
+ function buildPdfResults(): string {
357
+ return `<div style="margin-bottom:40px;background:#f9f9f9;padding:25px;border-radius:8px;">
358
+ <h2 style="margin:0 0 20px 0;font-size:18px;border-bottom:1px solid #ddd;padding-bottom:10px;color:#003b73;">Resumen de Resultados</h2>
359
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:30px;">
360
+ <div style="text-align:center;border-right:1px solid #ddd;">
361
+ <span style="display:block;font-size:12px;text-transform:uppercase;color:#666;margin-bottom:5px;">Ratio SA/D</span>
362
+ <span style="font-size:48px;font-weight:bold;color:#003b73;">${sadRatioDisplay.textContent}</span>
363
+ <div style="margin-top:10px;font-weight:bold;color:#ef4135;font-size:14px;">${verdictTitle.textContent}</div>
364
+ </div>
365
+ <div style="text-align:center;">
366
+ <span style="display:block;font-size:12px;text-transform:uppercase;color:#666;margin-bottom:5px;">Superficie Velica Total</span>
367
+ <span style="font-size:48px;font-weight:bold;color:#003b73;">${totalAreaDisplay.textContent}</span>
368
+ <div style="margin-top:10px;color:#666;font-size:14px;">Rendimiento: <strong>${powerIndicator.textContent}</strong></div>
369
+ </div>
370
+ </div>
371
+ </div>`;
372
+ }
373
+
374
+ function buildPdfHtml(): string {
375
+ const date = new Date().toLocaleDateString();
376
+ const header = `<div style="border-bottom:3px solid #003b73;padding-bottom:20px;margin-bottom:30px;display:flex;justify-content:space-between;align-items:flex-end;">
377
+ <div><h1 style="margin:0;color:#003b73;font-size:28px;text-transform:uppercase;">Informe Tecnico de Superficie Velica</h1>
378
+ <p style="margin:5px 0 0;color:#666;font-size:14px;">Generado por jjlmoya.es • ${date}</p></div>
379
+ <div style="text-align:right;"><span style="font-size:12px;font-weight:bold;color:#003b73;">VERSION PROFESIONAL</span></div>
380
+ </div>`;
381
+ const note = `<div style="background:#eef2f6;padding:20px;border-radius:8px;font-size:12px;color:#444;border-left:4px solid #003b73;">
382
+ <strong>Nota Tecnica:</strong> El ratio SA/D es un indicador del rendimiento potencial del barco.
383
+ </div>`;
384
+ const footer = `<div style="margin-top:40px;text-align:center;border-top:1px solid #eee;padding-top:20px;">
385
+ <p style="font-size:10px;color:#999;">Documento generado automaticamente por el calculador de jjlmoya.es.</p>
386
+ </div>`;
387
+ return `<div style="padding:40px;color:#333;max-width:800px;margin:0 auto;">${header}${buildPdfResults()}<h2 style="font-size:16px;text-transform:uppercase;color:#003b73;margin-bottom:15px;">Especificaciones Tecnicas</h2>${buildPdfTable()}${note}${footer}</div>`;
388
+ }
389
+
390
+ document.getElementById('btn-export-pdf-premium')?.addEventListener('click', async () => {
391
+ const element = document.getElementById('calculator-capture-area');
392
+ if (!element) return;
393
+ try {
394
+ const [jsPDFMod, html2canvasMod] = await Promise.all([import('jspdf'), import('html2canvas')]);
395
+ const jsPDF = jsPDFMod.default;
396
+ const html2canvas = html2canvasMod.default;
397
+ const canvas = await html2canvas(element, {
398
+ scale: 2,
399
+ useCORS: true,
400
+ backgroundColor: '#ffffff',
401
+ ignoreElements: (el) => el.classList.contains('sa-ignore-capture'),
402
+ onclone: (clonedDoc) => {
403
+ const clonedEl = clonedDoc.getElementById('calculator-capture-area');
404
+ if (clonedEl) clonedEl.innerHTML = buildPdfHtml();
405
+ },
406
+ });
407
+ const imgWidth = 595.28;
408
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
409
+ const pdf = new jsPDF('p', 'pt', 'a4');
410
+ pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, 0, imgWidth, imgHeight);
411
+ pdf.save(`informe-superficie-velica-${new Date().getTime()}.pdf`);
412
+ } catch (err) {
413
+ console.error(err);
414
+ }
415
+ });
416
+
417
+ loadFromLocal();
418
+ </script>