@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,351 @@
1
+ .sail-area-calculator {
2
+ --sa-primary: #003b73;
3
+ --sa-primary-light: #0055a4;
4
+ --sa-secondary: #ef4135;
5
+ --sa-radius: 16px;
6
+ --n-bg: #fff;
7
+ --n-bg-muted: #f1f5f9;
8
+ --n-text: #0f172a;
9
+ --n-text-muted: #475569;
10
+ --n-text-dim: #64748b;
11
+ --n-border: #e2e8f0;
12
+ --n-shadow: rgba(0, 0, 0, 0.05);
13
+ --n-primary: #3b82f6;
14
+ --n-primary-on: #fff;
15
+ --n-accent: #6366f1;
16
+ --n-cyan: #0891b2;
17
+ --n-success: #10b981;
18
+ --n-warning: #f59e0b;
19
+ --n-error: #f43f5e;
20
+
21
+ padding: 1rem;
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ .theme-dark .sail-area-calculator {
27
+ --n-bg: #0f172a;
28
+ --n-bg-muted: #1e293b;
29
+ --n-text: #f8fafc;
30
+ --n-text-muted: #94a3b8;
31
+ --n-text-dim: #64748b;
32
+ --n-border: #334155;
33
+ --n-shadow: rgba(0, 0, 0, 0.3);
34
+ --n-primary: #60a5fa;
35
+ --n-primary-on: #fff;
36
+ --n-accent: #818cf8;
37
+ --n-cyan: #22d3ee;
38
+ --n-success: #34d399;
39
+ --n-warning: #fbbf24;
40
+ --n-error: #fb7185;
41
+ }
42
+
43
+ .sa-calculator-wrapper {
44
+ background: var(--n-bg);
45
+ border: 1px solid var(--n-border);
46
+ border-radius: var(--sa-radius);
47
+ padding: 1.5rem;
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 2rem;
51
+ }
52
+
53
+ .sa-main-controls {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 1.5rem;
57
+ }
58
+
59
+ .sa-section-group h3 {
60
+ font-size: 0.75rem;
61
+ text-transform: uppercase;
62
+ letter-spacing: 0.1em;
63
+ color: var(--n-text-muted);
64
+ margin-bottom: 1rem;
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 0.5rem;
68
+ opacity: 0.8;
69
+ border-bottom: 1px solid var(--n-border);
70
+ padding-bottom: 0.4rem;
71
+ }
72
+
73
+ .sa-compact-grid {
74
+ display: grid;
75
+ grid-template-columns: repeat(2, 1fr);
76
+ gap: 1rem;
77
+ }
78
+
79
+ .sa-input-row {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 0.4rem;
83
+ }
84
+
85
+ .sa-input-row label {
86
+ font-size: 0.7rem;
87
+ font-weight: 700;
88
+ color: var(--n-text-muted);
89
+ }
90
+
91
+ .sa-elegant-input {
92
+ background: var(--n-bg-muted);
93
+ border: 1.5px solid var(--n-border);
94
+ border-radius: 10px;
95
+ padding: 0.7rem 0.9rem;
96
+ font-size: 0.95rem;
97
+ color: var(--n-text);
98
+ font-weight: 600;
99
+ width: 100%;
100
+ }
101
+
102
+ .sa-elegant-input:focus {
103
+ border-color: var(--sa-primary-light);
104
+ outline: none;
105
+ }
106
+
107
+ .sa-input-with-unit {
108
+ position: relative;
109
+ display: flex;
110
+ align-items: center;
111
+ }
112
+
113
+ .sa-unit-tag {
114
+ position: absolute;
115
+ right: 10px;
116
+ font-size: 0.65rem;
117
+ font-weight: 900;
118
+ color: var(--n-text-muted);
119
+ background: var(--n-bg);
120
+ border: 1px solid var(--n-border);
121
+ padding: 2px 6px;
122
+ border-radius: 4px;
123
+ }
124
+
125
+ .sa-unit-tag select {
126
+ color: var(--n-text-muted);
127
+ }
128
+
129
+ .sa-sidebar-results {
130
+ background: var(--n-bg-muted);
131
+ padding: 1.5rem;
132
+ border-radius: var(--sa-radius);
133
+ border: 1px solid var(--n-border);
134
+ text-align: center;
135
+ }
136
+
137
+ .sad-value-large {
138
+ font-size: 3.5rem;
139
+ font-weight: 950;
140
+ letter-spacing: -0.04em;
141
+ color: var(--sa-primary);
142
+ line-height: 1;
143
+ }
144
+
145
+ .theme-dark .sail-area-calculator .sad-value-large {
146
+ color: var(--n-accent);
147
+ }
148
+
149
+ .sad-label {
150
+ font-size: 0.75rem;
151
+ font-weight: 850;
152
+ color: var(--n-text-muted);
153
+ text-transform: uppercase;
154
+ letter-spacing: 0.15em;
155
+ }
156
+
157
+ .sa-gauge-track {
158
+ height: 4px;
159
+ background: var(--n-border);
160
+ border-radius: 2px;
161
+ margin: 1.5rem 0;
162
+ position: relative;
163
+ }
164
+
165
+ .sa-gauge-indicator {
166
+ position: absolute;
167
+ top: 50%;
168
+ width: 14px;
169
+ height: 14px;
170
+ background: var(--sa-secondary);
171
+ border: 3px solid var(--n-bg-muted);
172
+ border-radius: 50%;
173
+ transform: translate(-50%, -50%);
174
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
175
+ transition: left 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
176
+ }
177
+
178
+ .sa-verdict-box {
179
+ background: var(--n-bg);
180
+ padding: 1rem;
181
+ border-radius: 12px;
182
+ border: 1px solid var(--n-border);
183
+ }
184
+
185
+ .sa-verdict-box h4 {
186
+ font-size: 0.95rem;
187
+ margin-bottom: 0.4rem;
188
+ color: var(--n-text);
189
+ }
190
+
191
+ .sa-verdict-box p {
192
+ font-size: 0.8rem;
193
+ line-height: 1.5;
194
+ color: var(--n-text-muted);
195
+ }
196
+
197
+ .sa-stats-summary {
198
+ display: grid;
199
+ grid-template-columns: 1fr 1fr;
200
+ gap: 1rem;
201
+ margin-top: 1.5rem;
202
+ padding-top: 1.5rem;
203
+ border-top: 1px dashed var(--n-border);
204
+ }
205
+
206
+ .sa-stat-item span {
207
+ font-size: 0.75rem;
208
+ color: var(--n-text-muted);
209
+ display: block;
210
+ }
211
+
212
+ .sa-stat-item span:last-child {
213
+ font-size: 1.2rem;
214
+ font-weight: 900;
215
+ color: var(--n-text);
216
+ }
217
+
218
+ .sa-export-btn-premium {
219
+ width: 100%;
220
+ margin-top: 1.5rem;
221
+ padding: 1rem;
222
+ background: var(--sa-primary);
223
+ color: var(--n-primary-on);
224
+ border: none;
225
+ border-radius: 10px;
226
+ font-size: 0.75rem;
227
+ font-weight: 800;
228
+ letter-spacing: 0.05em;
229
+ cursor: pointer;
230
+ display: flex;
231
+ align-items: center;
232
+ justify-content: center;
233
+ gap: 0.5rem;
234
+ }
235
+
236
+ .sa-slider-box {
237
+ background: var(--n-bg-muted);
238
+ padding: 1.5rem;
239
+ border-radius: 12px;
240
+ border: 1px solid var(--n-border);
241
+ }
242
+
243
+ .sa-slider-header {
244
+ display: flex;
245
+ justify-content: space-between;
246
+ align-items: center;
247
+ margin-bottom: 1.5rem;
248
+ }
249
+
250
+ .sa-slider-header label {
251
+ font-size: 0.85rem;
252
+ font-weight: 800;
253
+ color: var(--n-text);
254
+ }
255
+
256
+ .sa-percentage-badge {
257
+ background: var(--sa-primary);
258
+ color: var(--n-primary-on);
259
+ padding: 4px 10px;
260
+ border-radius: 20px;
261
+ font-size: 0.8rem;
262
+ font-weight: 900;
263
+ }
264
+
265
+ .sa-slider-wrapper {
266
+ padding: 0 5px;
267
+ }
268
+
269
+ .sa-modern-slider {
270
+ appearance: none;
271
+ width: 100%;
272
+ height: 6px;
273
+ background: var(--n-border);
274
+ border-radius: 10px;
275
+ outline: none;
276
+ margin: 1rem 0;
277
+ }
278
+
279
+ .sa-modern-slider::-webkit-slider-thumb {
280
+ appearance: none;
281
+ width: 24px;
282
+ height: 24px;
283
+ background: var(--sa-primary);
284
+ border: 4px solid var(--n-bg-muted);
285
+ border-radius: 50%;
286
+ cursor: pointer;
287
+ box-shadow: 0 2px 10px rgba(0, 59, 115, 0.2);
288
+ transition: transform 0.2s;
289
+ }
290
+
291
+ .sa-modern-slider::-webkit-slider-thumb:hover {
292
+ transform: scale(1.1);
293
+ }
294
+
295
+ .sa-modern-slider::-moz-range-thumb {
296
+ width: 24px;
297
+ height: 24px;
298
+ background: var(--sa-primary);
299
+ border: 4px solid var(--n-bg-muted);
300
+ border-radius: 50%;
301
+ cursor: pointer;
302
+ }
303
+
304
+ .sa-modern-slider::-moz-range-track {
305
+ height: 6px;
306
+ background: var(--n-border);
307
+ border-radius: 10px;
308
+ }
309
+
310
+ .sa-slider-ticks {
311
+ display: flex;
312
+ justify-content: space-between;
313
+ margin-top: 0.5rem;
314
+ }
315
+
316
+ .sa-slider-ticks span {
317
+ font-size: 0.65rem;
318
+ font-weight: 700;
319
+ color: var(--n-text-muted);
320
+ }
321
+
322
+ @media (min-width: 1024px) {
323
+ .sail-area-calculator {
324
+ padding: 2rem;
325
+ }
326
+
327
+ .sa-calculator-wrapper {
328
+ display: grid;
329
+ grid-template-columns: 1fr 400px;
330
+ gap: 3rem;
331
+ padding: 2.5rem;
332
+ }
333
+
334
+ .sa-compact-grid {
335
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
336
+ }
337
+
338
+ .sa-main-controls {
339
+ gap: 2.5rem;
340
+ }
341
+
342
+ .sa-sidebar-results {
343
+ position: sticky;
344
+ top: 2rem;
345
+ padding: 2.5rem;
346
+ }
347
+
348
+ .sad-value-large {
349
+ font-size: 5.5rem;
350
+ }
351
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { speedConverter } 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 speedConverter.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} title={content.ui.bibliographyTitle} />}
@@ -0,0 +1,125 @@
1
+ ---
2
+ import './style.css';
3
+ import type { SpeedConverterUI } from './index';
4
+ interface Props { ui: SpeedConverterUI; }
5
+ const { ui } = Astro.props;
6
+ const bd = JSON.parse(ui.beaufortDataJson) as { force: number; name: string; sea: string; effect: string; kn: string }[];
7
+ ---
8
+ <section id="speed-converter-root" class="speed-converter" data-ui={JSON.stringify(ui)}>
9
+ <div class="sc-input-grid">
10
+ <div class="sc-input-group">
11
+ <label for="val-kn">{ui.knLabel}</label>
12
+ <div class="sc-input-wrapper"><input type="number" id="val-kn" placeholder="0.0" step="0.1" /></div>
13
+ </div>
14
+ <div class="sc-input-group">
15
+ <label for="val-kmh">{ui.kmhLabel}</label>
16
+ <div class="sc-input-wrapper"><input type="number" id="val-kmh" placeholder="0.0" step="0.1" /></div>
17
+ </div>
18
+ <div class="sc-input-group">
19
+ <label for="val-ms">{ui.msLabel}</label>
20
+ <div class="sc-input-wrapper"><input type="number" id="val-ms" placeholder="0.0" step="0.1" /></div>
21
+ </div>
22
+ <div class="sc-input-group">
23
+ <label for="val-mph">{ui.mphLabel}</label>
24
+ <div class="sc-input-wrapper"><input type="number" id="val-mph" placeholder="0.0" step="0.1" /></div>
25
+ </div>
26
+ </div>
27
+ <div class="sc-beaufort-summary">
28
+ <div class="sc-force-circle" id="f-num">0</div>
29
+ <div class="sc-force-text-box">
30
+ <h2 id="f-desc" class="sc-force-name">{bd[0]?.name ?? ''}</h2>
31
+ <p id="f-effect" class="sc-force-detail">{bd[0]?.effect ?? ''}</p>
32
+ </div>
33
+ <div class="sc-sea-state-box">
34
+ <div class="sc-sea-text">
35
+ <span class="label">{ui.seaStateLabel}</span>
36
+ <p id="f-sea">{bd[0]?.sea ?? ''}</p>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ <div class="sc-table-container">
41
+ <h3>{ui.beaufortTitle}</h3>
42
+ <div class="sc-scroll-area">
43
+ <table class="sc-beaufort-table">
44
+ <thead>
45
+ <tr>
46
+ <th>{ui.forceLabel}</th><th>{ui.descriptionLabel}</th>
47
+ <th>{ui.knotsLabel}</th><th>{ui.effectLabel}</th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ {bd.map((b) => (
52
+ <tr data-force={b.force} class="sc-clickable-row">
53
+ <td><span class="sc-force-tag">F{b.force}</span></td>
54
+ <td><strong>{b.name}</strong></td>
55
+ <td>{b.kn}</td>
56
+ <td>{b.sea}</td>
57
+ </tr>
58
+ ))}
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+ </div>
63
+ </section>
64
+ <script>
65
+ const root = document.getElementById('speed-converter-root') as HTMLElement;
66
+ const ui = JSON.parse(root.dataset.ui as string);
67
+ const bd = JSON.parse(ui.beaufortDataJson) as { force: number; name: string; sea: string; effect: string; kn: string }[];
68
+ const iKn = document.getElementById('val-kn') as HTMLInputElement;
69
+ const iKmh = document.getElementById('val-kmh') as HTMLInputElement;
70
+ const iMs = document.getElementById('val-ms') as HTMLInputElement;
71
+ const iMph = document.getElementById('val-mph') as HTMLInputElement;
72
+ const fNum = document.getElementById('f-num') as HTMLElement;
73
+ const fDesc = document.getElementById('f-desc') as HTMLElement;
74
+ const fEff = document.getElementById('f-effect') as HTMLElement;
75
+ const fSea = document.getElementById('f-sea') as HTMLElement;
76
+ const rows = document.querySelectorAll<HTMLTableRowElement>('.sc-clickable-row');
77
+ const KMH = 1.852, MS = 0.514444, MPH = 1.15078;
78
+
79
+ function getForce(kn: number): number {
80
+ const lim = [1,4,7,11,17,22,28,34,41,48,56,64];
81
+ for (let i = 0; i < lim.length; i++) { if (kn < (lim[i] ?? 999)) return i; }
82
+ return 12;
83
+ }
84
+
85
+ function renderBeaufort(kn: number) {
86
+ const f = getForce(kn);
87
+ const d = bd[f];
88
+ if (!d) return;
89
+ fNum.textContent = String(f);
90
+ fDesc.textContent = d.name;
91
+ fEff.textContent = d.effect;
92
+ fSea.textContent = d.sea;
93
+ rows.forEach((r) => r.classList.toggle('active', parseInt(r.dataset.force ?? '-1') === f));
94
+ }
95
+
96
+ function sync(skip: string, kn: number) {
97
+ if (skip !== 'val-kn') iKn.value = kn.toFixed(1);
98
+ if (skip !== 'val-kmh') iKmh.value = (kn * KMH).toFixed(1);
99
+ if (skip !== 'val-ms') iMs.value = (kn * MS).toFixed(2);
100
+ if (skip !== 'val-mph') iMph.value = (kn * MPH).toFixed(2);
101
+ renderBeaufort(kn);
102
+ localStorage.setItem('jjlmoya-nautical-speed', String(kn));
103
+ }
104
+
105
+ iKn.addEventListener('input', (e) => sync('val-kn', parseFloat((e.target as HTMLInputElement).value)||0));
106
+ iKmh.addEventListener('input', (e) => sync('val-kmh', (parseFloat((e.target as HTMLInputElement).value)||0)/KMH));
107
+ iMs.addEventListener('input', (e) => sync('val-ms', (parseFloat((e.target as HTMLInputElement).value)||0)/MS));
108
+ iMph.addEventListener('input', (e) => sync('val-mph', (parseFloat((e.target as HTMLInputElement).value)||0)/MPH));
109
+
110
+ rows.forEach((row) => {
111
+ row.addEventListener('click', () => {
112
+ const f = parseInt(row.dataset.force ?? '0');
113
+ const d = bd[f];
114
+ let kn = 0;
115
+ if (f === 12) { kn = 65; }
116
+ else if (f > 0) { kn = parseFloat(d?.kn.split('-')[0] ?? '0') || 0; }
117
+ iKn.value = String(kn);
118
+ sync('val-kn', kn);
119
+ });
120
+ });
121
+
122
+ const savedKn = parseFloat(localStorage.getItem('jjlmoya-nautical-speed') ?? '0') || 0;
123
+ iKn.value = savedKn > 0 ? String(savedKn) : '';
124
+ sync('val-kn', savedKn);
125
+ </script>