@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,481 @@
1
+ ---
2
+ import './style.css';
3
+ import type { NauticalConverterUI } from './index';
4
+
5
+ interface Props {
6
+ ui: NauticalConverterUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+
11
+ const beaufortEs = [
12
+ { name: 'Calma', sea: 'Mar como un espejo' },
13
+ { name: 'Ventolina', sea: 'Rizos sin espuma' },
14
+ { name: 'Flojito', sea: 'Olas pequeñas' },
15
+ { name: 'Flojo', sea: 'Ovejas blancas' },
16
+ { name: 'Bonancible', sea: 'Borreguillos frecuentes' },
17
+ { name: 'Fresquito', sea: 'Olas moderadas con crestas' },
18
+ { name: 'Fresco', sea: 'Crestas blancas extensas' },
19
+ { name: 'Frescachon', sea: 'Espuma en hileras' },
20
+ { name: 'Temporal', sea: 'Rompientes en crestas' },
21
+ { name: 'Temporal fuerte', sea: 'Olas muy altas' },
22
+ { name: 'Temporal duro', sea: 'Mar blanca' },
23
+ { name: 'Temporal muy duro', sea: 'Espuma cubre el mar' },
24
+ { name: 'Huracan', sea: 'Aire lleno de espuma' },
25
+ ];
26
+
27
+ const beaufortEn = [
28
+ { name: 'Calm', sea: 'Flat sea' },
29
+ { name: 'Light air', sea: 'Ripples without crests' },
30
+ { name: 'Light breeze', sea: 'Small wavelets' },
31
+ { name: 'Gentle breeze', sea: 'Scattered whitecaps' },
32
+ { name: 'Moderate breeze', sea: 'Frequent whitecaps' },
33
+ { name: 'Fresh breeze', sea: 'Moderate waves' },
34
+ { name: 'Strong breeze', sea: 'Large waves' },
35
+ { name: 'Near gale', sea: 'Sea heaps up' },
36
+ { name: 'Gale', sea: 'High waves' },
37
+ { name: 'Strong gale', sea: 'Very high waves' },
38
+ { name: 'Storm', sea: 'Exceptionally high waves' },
39
+ { name: 'Violent storm', sea: 'Phenomenal' },
40
+ { name: 'Hurricane', sea: 'Air filled with foam' },
41
+ ];
42
+
43
+ const beaufortFr = [
44
+ { name: 'Calme', sea: 'Mer comme un miroir' },
45
+ { name: 'Vent tres leger', sea: 'Rides' },
46
+ { name: 'Legere brise', sea: 'Petites vaguelettes' },
47
+ { name: 'Petite brise', sea: 'Quelques moutons' },
48
+ { name: 'Jolie brise', sea: 'Moutons frequents' },
49
+ { name: 'Bonne brise', sea: 'Vagues moderees' },
50
+ { name: 'Vent frais', sea: 'Grandes vagues' },
51
+ { name: 'Grand frais', sea: 'Lames deferlantes' },
52
+ { name: 'Coup de vent', sea: 'Vagues tres hautes' },
53
+ { name: 'Fort coup de vent', sea: 'Enormes vagues' },
54
+ { name: 'Tempete', sea: 'Tres grosses vagues' },
55
+ { name: 'Violente tempete', sea: 'Phenomenal' },
56
+ { name: 'Ouragan', sea: 'Air plein d\'ecume' },
57
+ ];
58
+
59
+ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
60
+ ---
61
+
62
+ <section
63
+ id="nautical-converter"
64
+ class="nautical-converter"
65
+ data-ui={JSON.stringify(ui)}
66
+ data-beaufort={JSON.stringify(allBeaufort)}
67
+ >
68
+ <div class="nc-category-tabs" role="tablist">
69
+ <button class="nc-tab-btn active" data-tab="distance" role="tab">{ui.distanceCategoryLabel}</button>
70
+ <button class="nc-tab-btn" data-tab="speed" role="tab">{ui.speedCategoryLabel}</button>
71
+ <button class="nc-tab-btn" data-tab="depth" role="tab">{ui.depthCategoryLabel}</button>
72
+ <button class="nc-tab-btn" data-tab="pressure" role="tab">{ui.pressureCategoryLabel}</button>
73
+ </div>
74
+
75
+ <div id="tab-distance" class="nc-tab-panel active">
76
+ <div class="nc-converter-card">
77
+ <div class="nc-input-grid">
78
+ <div class="nc-input-group">
79
+ <label>{ui.nmLabel}</label>
80
+ <div class="nc-input-wrapper">
81
+ <input class="nc-elegant-input" type="number" id="dist-nm" step="any" placeholder="0" />
82
+ <button class="nc-copy-btn" data-copy="dist-nm" title={ui.copyLabel}>
83
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
84
+ </button>
85
+ </div>
86
+ </div>
87
+ <div class="nc-input-group">
88
+ <label>{ui.kmLabel}</label>
89
+ <div class="nc-input-wrapper">
90
+ <input class="nc-elegant-input" type="number" id="dist-km" step="any" placeholder="0" />
91
+ <button class="nc-copy-btn" data-copy="dist-km" title={ui.copyLabel}>
92
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
93
+ </button>
94
+ </div>
95
+ </div>
96
+ <div class="nc-input-group">
97
+ <label>{ui.miLabel}</label>
98
+ <div class="nc-input-wrapper">
99
+ <input class="nc-elegant-input" type="number" id="dist-mi" step="any" placeholder="0" />
100
+ <button class="nc-copy-btn" data-copy="dist-mi" title={ui.copyLabel}>
101
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
102
+ </button>
103
+ </div>
104
+ </div>
105
+ <div class="nc-input-group">
106
+ <label>{ui.cableLabel}</label>
107
+ <div class="nc-input-wrapper">
108
+ <input class="nc-elegant-input" type="number" id="dist-cable" step="any" placeholder="0" />
109
+ <button class="nc-copy-btn" data-copy="dist-cable" title={ui.copyLabel}>
110
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
111
+ </button>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <div id="tab-speed" class="nc-tab-panel">
119
+ <div class="nc-converter-card">
120
+ <div class="nc-input-grid">
121
+ <div class="nc-input-group">
122
+ <label>{ui.knLabel}</label>
123
+ <div class="nc-input-wrapper">
124
+ <input class="nc-elegant-input" type="number" id="speed-kn" step="any" placeholder="0" />
125
+ <button class="nc-copy-btn" data-copy="speed-kn" title={ui.copyLabel}>
126
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
127
+ </button>
128
+ </div>
129
+ </div>
130
+ <div class="nc-input-group">
131
+ <label>{ui.kmhLabel}</label>
132
+ <div class="nc-input-wrapper">
133
+ <input class="nc-elegant-input" type="number" id="speed-kmh" step="any" placeholder="0" />
134
+ <button class="nc-copy-btn" data-copy="speed-kmh" title={ui.copyLabel}>
135
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
136
+ </button>
137
+ </div>
138
+ </div>
139
+ <div class="nc-input-group">
140
+ <label>{ui.msLabel}</label>
141
+ <div class="nc-input-wrapper">
142
+ <input class="nc-elegant-input" type="number" id="speed-ms" step="any" placeholder="0" />
143
+ <button class="nc-copy-btn" data-copy="speed-ms" title={ui.copyLabel}>
144
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
145
+ </button>
146
+ </div>
147
+ </div>
148
+ <div class="nc-input-group">
149
+ <label>{ui.mphLabel}</label>
150
+ <div class="nc-input-wrapper">
151
+ <input class="nc-elegant-input" type="number" id="speed-mph" step="any" placeholder="0" />
152
+ <button class="nc-copy-btn" data-copy="speed-mph" title={ui.copyLabel}>
153
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
154
+ </button>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ <div class="nc-beaufort-card" id="beaufort-card">
159
+ <div class="nc-beaufort-badge" id="beaufort-badge">0</div>
160
+ <div class="nc-beaufort-info">
161
+ <span class="nc-beaufort-name" id="beaufort-name"></span>
162
+ <span class="nc-beaufort-sea" id="beaufort-sea"></span>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <div id="tab-depth" class="nc-tab-panel">
169
+ <div class="nc-converter-card">
170
+ <div class="nc-input-grid">
171
+ <div class="nc-input-group">
172
+ <label>{ui.brozaLabel}</label>
173
+ <div class="nc-input-wrapper">
174
+ <input class="nc-elegant-input" type="number" id="depth-braza" step="any" placeholder="0" />
175
+ <button class="nc-copy-btn" data-copy="depth-braza" title={ui.copyLabel}>
176
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
177
+ </button>
178
+ </div>
179
+ </div>
180
+ <div class="nc-input-group">
181
+ <label>{ui.mLabel}</label>
182
+ <div class="nc-input-wrapper">
183
+ <input class="nc-elegant-input" type="number" id="depth-m" step="any" placeholder="0" />
184
+ <button class="nc-copy-btn" data-copy="depth-m" title={ui.copyLabel}>
185
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
186
+ </button>
187
+ </div>
188
+ </div>
189
+ <div class="nc-input-group">
190
+ <label>{ui.ftLabel}</label>
191
+ <div class="nc-input-wrapper">
192
+ <input class="nc-elegant-input" type="number" id="depth-ft" step="any" placeholder="0" />
193
+ <button class="nc-copy-btn" data-copy="depth-ft" title={ui.copyLabel}>
194
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
195
+ </button>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <div id="tab-pressure" class="nc-tab-panel">
203
+ <div class="nc-converter-card">
204
+ <div class="nc-input-grid">
205
+ <div class="nc-input-group">
206
+ <label>{ui.mbarLabel}</label>
207
+ <div class="nc-input-wrapper">
208
+ <input class="nc-elegant-input" type="number" id="pres-mbar" step="any" placeholder="1013" />
209
+ <button class="nc-copy-btn" data-copy="pres-mbar" title={ui.copyLabel}>
210
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
211
+ </button>
212
+ </div>
213
+ </div>
214
+ <div class="nc-input-group">
215
+ <label>{ui.mmhgLabel}</label>
216
+ <div class="nc-input-wrapper">
217
+ <input class="nc-elegant-input" type="number" id="pres-mmhg" step="any" placeholder="760" />
218
+ <button class="nc-copy-btn" data-copy="pres-mmhg" title={ui.copyLabel}>
219
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
220
+ </button>
221
+ </div>
222
+ </div>
223
+ <div class="nc-input-group">
224
+ <label>{ui.inhgLabel}</label>
225
+ <div class="nc-input-wrapper">
226
+ <input class="nc-elegant-input" type="number" id="pres-inhg" step="any" placeholder="29.92" />
227
+ <button class="nc-copy-btn" data-copy="pres-inhg" title={ui.copyLabel}>
228
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
229
+ </button>
230
+ </div>
231
+ </div>
232
+ <div class="nc-input-group">
233
+ <label>{ui.atmLabel}</label>
234
+ <div class="nc-input-wrapper">
235
+ <input class="nc-elegant-input" type="number" id="pres-atm" step="any" placeholder="1" />
236
+ <button class="nc-copy-btn" data-copy="pres-atm" title={ui.copyLabel}>
237
+ <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
238
+ </button>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </section>
245
+
246
+ <script>
247
+ const section = document.getElementById('nautical-converter') as HTMLElement;
248
+ const ui = JSON.parse(section.dataset.ui as string);
249
+ const allBeaufort = JSON.parse(section.dataset.beaufort as string) as Record<string, { name: string; sea: string }[]>;
250
+
251
+ const BEAUFORT_THRESHOLDS = [1, 3, 6, 10, 16, 21, 27, 33, 40, 47, 55, 63];
252
+ const BEAUFORT_COLORS = [
253
+ '#3b82f6', '#60a5fa', '#34d399', '#a3e635',
254
+ '#facc15', '#fb923c', '#f87171', '#ef4444',
255
+ '#dc2626', '#b91c1c', '#7f1d1d', '#4c0519', '#1a0010',
256
+ ];
257
+
258
+ function detectLocale(): string {
259
+ const html = document.documentElement.lang || 'es';
260
+ const short = html.split('-')[0] ?? 'es';
261
+ return ['es', 'en', 'fr'].includes(short) ? short : 'es';
262
+ }
263
+
264
+ function getBeaufortForce(knots: number): number {
265
+ const idx = BEAUFORT_THRESHOLDS.findIndex((t) => knots < t);
266
+ return idx === -1 ? 12 : idx;
267
+ }
268
+
269
+ function fmt(n: number): string {
270
+ if (Math.abs(n) >= 1000) return n.toFixed(2);
271
+ if (Math.abs(n) >= 10) return n.toFixed(3);
272
+ return n.toFixed(4);
273
+ }
274
+
275
+ function setVal(id: string, val: number) {
276
+ const el = document.getElementById(id) as HTMLInputElement;
277
+ if (el) el.value = fmt(val);
278
+ }
279
+
280
+ function getVal(id: string): number {
281
+ const el = document.getElementById(id) as HTMLInputElement;
282
+ return parseFloat(el?.value ?? '');
283
+ }
284
+
285
+ interface BeaufortElements {
286
+ card: HTMLElement;
287
+ badge: HTMLElement;
288
+ nameEl: HTMLElement;
289
+ seaEl: HTMLElement;
290
+ knots: number;
291
+ locale: string;
292
+ }
293
+
294
+ function renderBeaufort(opts: BeaufortElements) {
295
+ const { card, badge, nameEl, seaEl, knots, locale } = opts;
296
+ const force = getBeaufortForce(Math.abs(knots));
297
+ const data = (allBeaufort[locale] ?? allBeaufort['es'] ?? [])[force];
298
+ card.classList.add('visible');
299
+ badge.textContent = String(force);
300
+ badge.style.background = BEAUFORT_COLORS[force] ?? '#3b82f6';
301
+ nameEl.textContent = `${ui.forceLabel} ${force} - ${data?.name ?? ''}`;
302
+ seaEl.textContent = `${ui.windSeaLabel}: ${data?.sea ?? ''}`;
303
+ }
304
+
305
+ function bindSpeedFromKnots(kn: number, onBeaufort: (k: number) => void) {
306
+ setVal('speed-kmh', kn * 1.852);
307
+ setVal('speed-ms', kn * 0.514444);
308
+ setVal('speed-mph', kn * 1.15078);
309
+ onBeaufort(kn);
310
+ }
311
+
312
+ function bindSpeedInputs(onBeaufort: (kn: number) => void) {
313
+ document.getElementById('speed-kn')?.addEventListener('input', () => {
314
+ const v = getVal('speed-kn');
315
+ if (!isNaN(v)) bindSpeedFromKnots(v, onBeaufort);
316
+ });
317
+ document.getElementById('speed-kmh')?.addEventListener('input', () => {
318
+ const v = getVal('speed-kmh');
319
+ if (isNaN(v)) return;
320
+ const kn = v / 1.852;
321
+ setVal('speed-kn', kn); setVal('speed-ms', kn * 0.514444); setVal('speed-mph', kn * 1.15078);
322
+ onBeaufort(kn);
323
+ });
324
+ document.getElementById('speed-ms')?.addEventListener('input', () => {
325
+ const v = getVal('speed-ms');
326
+ if (isNaN(v)) return;
327
+ const kn = v / 0.514444;
328
+ setVal('speed-kn', kn); setVal('speed-kmh', kn * 1.852); setVal('speed-mph', kn * 1.15078);
329
+ onBeaufort(kn);
330
+ });
331
+ document.getElementById('speed-mph')?.addEventListener('input', () => {
332
+ const v = getVal('speed-mph');
333
+ if (isNaN(v)) return;
334
+ const kn = v / 1.15078;
335
+ setVal('speed-kn', kn); setVal('speed-kmh', kn * 1.852); setVal('speed-ms', kn * 0.514444);
336
+ onBeaufort(kn);
337
+ });
338
+ }
339
+
340
+ function bindDepthInputs() {
341
+ const bToM = 1.8288;
342
+ const ftToM = 0.3048;
343
+ document.getElementById('depth-braza')?.addEventListener('input', () => {
344
+ const v = getVal('depth-braza');
345
+ if (isNaN(v)) return;
346
+ const m = v * bToM;
347
+ setVal('depth-m', m); setVal('depth-ft', m / ftToM);
348
+ });
349
+ document.getElementById('depth-m')?.addEventListener('input', () => {
350
+ const v = getVal('depth-m');
351
+ if (isNaN(v)) return;
352
+ setVal('depth-braza', v / bToM); setVal('depth-ft', v / ftToM);
353
+ });
354
+ document.getElementById('depth-ft')?.addEventListener('input', () => {
355
+ const v = getVal('depth-ft');
356
+ if (isNaN(v)) return;
357
+ const m = v * ftToM;
358
+ setVal('depth-m', m); setVal('depth-braza', m / bToM);
359
+ });
360
+ }
361
+
362
+ function bindPressureFromMbar(mb: number) {
363
+ setVal('pres-mmhg', mb / 1.33322);
364
+ setVal('pres-inhg', mb / 33.8639);
365
+ setVal('pres-atm', mb / 1013.25);
366
+ }
367
+
368
+ function bindPressureInputs() {
369
+ document.getElementById('pres-mbar')?.addEventListener('input', () => {
370
+ const v = getVal('pres-mbar');
371
+ if (!isNaN(v)) bindPressureFromMbar(v);
372
+ });
373
+ document.getElementById('pres-mmhg')?.addEventListener('input', () => {
374
+ const v = getVal('pres-mmhg');
375
+ if (isNaN(v)) return;
376
+ const mb = v * 1.33322;
377
+ setVal('pres-mbar', mb); setVal('pres-inhg', mb / 33.8639); setVal('pres-atm', mb / 1013.25);
378
+ });
379
+ document.getElementById('pres-inhg')?.addEventListener('input', () => {
380
+ const v = getVal('pres-inhg');
381
+ if (isNaN(v)) return;
382
+ const mb = v * 33.8639;
383
+ setVal('pres-mbar', mb); setVal('pres-mmhg', mb / 1.33322); setVal('pres-atm', mb / 1013.25);
384
+ });
385
+ document.getElementById('pres-atm')?.addEventListener('input', () => {
386
+ const v = getVal('pres-atm');
387
+ if (isNaN(v)) return;
388
+ const mb = v * 1013.25;
389
+ setVal('pres-mbar', mb); setVal('pres-mmhg', mb / 1.33322); setVal('pres-inhg', mb / 33.8639);
390
+ });
391
+ }
392
+
393
+ class NauticalConverter {
394
+ private locale: string;
395
+
396
+ constructor() {
397
+ this.locale = detectLocale();
398
+ this.setupTabs();
399
+ this.setupDistance();
400
+ this.setupSpeed();
401
+ this.setupDepth();
402
+ this.setupPressure();
403
+ this.setupCopyButtons();
404
+ }
405
+
406
+ private setupTabs() {
407
+ const tabs = section.querySelectorAll<HTMLButtonElement>('.nc-tab-btn');
408
+ const lastCat = localStorage.getItem('nautical-last-category');
409
+ if (lastCat) {
410
+ tabs.forEach((t) => t.classList.remove('active'));
411
+ section.querySelectorAll('.nc-tab-panel').forEach((p) => p.classList.remove('active'));
412
+ const active = section.querySelector<HTMLButtonElement>(`.nc-tab-btn[data-tab="${lastCat}"]`);
413
+ if (active) {
414
+ active.classList.add('active');
415
+ section.querySelector(`#tab-${lastCat}`)?.classList.add('active');
416
+ }
417
+ }
418
+ tabs.forEach((btn) => {
419
+ btn.addEventListener('click', () => {
420
+ tabs.forEach((t) => t.classList.remove('active'));
421
+ section.querySelectorAll('.nc-tab-panel').forEach((p) => p.classList.remove('active'));
422
+ btn.classList.add('active');
423
+ const panel = section.querySelector(`#tab-${btn.dataset.tab}`);
424
+ panel?.classList.add('active');
425
+ localStorage.setItem('nautical-last-category', btn.dataset.tab ?? '');
426
+ });
427
+ });
428
+ }
429
+
430
+ private setupDistance() {
431
+ const ids = ['dist-nm', 'dist-km', 'dist-mi', 'dist-cable'];
432
+ const toMeters = [1852, 1000, 1609.34, 185.2];
433
+ ids.forEach((id, i) => {
434
+ document.getElementById(id)?.addEventListener('input', () => {
435
+ const v = getVal(id);
436
+ if (isNaN(v)) return;
437
+ const meters = v * (toMeters[i] ?? 1);
438
+ ids.forEach((tid, j) => {
439
+ if (tid !== id) setVal(tid, meters / (toMeters[j] ?? 1));
440
+ });
441
+ });
442
+ });
443
+ }
444
+
445
+ private updateBeaufort(knots: number) {
446
+ const card = document.getElementById('beaufort-card');
447
+ const badge = document.getElementById('beaufort-badge');
448
+ const nameEl = document.getElementById('beaufort-name');
449
+ const seaEl = document.getElementById('beaufort-sea');
450
+ if (!card || !badge || !nameEl || !seaEl) return;
451
+ if (isNaN(knots)) { card.classList.remove('visible'); return; }
452
+ renderBeaufort({ card, badge, nameEl, seaEl, knots, locale: this.locale });
453
+ }
454
+
455
+ private setupSpeed() {
456
+ bindSpeedInputs((kn: number) => this.updateBeaufort(kn));
457
+ }
458
+
459
+ private setupDepth() {
460
+ bindDepthInputs();
461
+ }
462
+
463
+ private setupPressure() {
464
+ bindPressureInputs();
465
+ }
466
+
467
+ private setupCopyButtons() {
468
+ section.querySelectorAll<HTMLButtonElement>('.nc-copy-btn').forEach((btn) => {
469
+ btn.addEventListener('click', () => {
470
+ const targetId = btn.dataset.copy;
471
+ if (!targetId) return;
472
+ const input = document.getElementById(targetId) as HTMLInputElement;
473
+ if (!input?.value) return;
474
+ navigator.clipboard.writeText(input.value).catch(() => null);
475
+ });
476
+ });
477
+ }
478
+ }
479
+
480
+ new NauticalConverter();
481
+ </script>