@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,310 @@
1
+ .tide-calculator {
2
+ --n-bg: #fff;
3
+ --n-bg-muted: #f1f5f9;
4
+ --n-text: #0f172a;
5
+ --n-text-muted: #475569;
6
+ --n-text-dim: #64748b;
7
+ --n-border: #e2e8f0;
8
+ --n-shadow: rgba(0, 0, 0, 0.05);
9
+ --n-primary: #3b82f6;
10
+ --n-primary-on: #fff;
11
+ --n-accent: #6366f1;
12
+ --n-cyan: #0891b2;
13
+ --n-success: #10b981;
14
+ --n-warning: #f59e0b;
15
+ --n-error: #f43f5e;
16
+
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 1.5rem;
20
+ max-width: 1000px;
21
+ margin: 0 auto;
22
+ }
23
+
24
+ .theme-dark .tide-calculator {
25
+ --n-bg: #0f172a;
26
+ --n-bg-muted: #1e293b;
27
+ --n-text: #f8fafc;
28
+ --n-text-muted: #94a3b8;
29
+ --n-text-dim: #64748b;
30
+ --n-border: #334155;
31
+ --n-shadow: rgba(0, 0, 0, 0.3);
32
+ --n-primary: #60a5fa;
33
+ --n-primary-on: #fff;
34
+ --n-accent: #818cf8;
35
+ --n-cyan: #22d3ee;
36
+ --n-success: #34d399;
37
+ --n-warning: #fbbf24;
38
+ --n-error: #fb7185;
39
+ }
40
+
41
+ .tc-main-card {
42
+ background: var(--n-bg);
43
+ border: 1px solid var(--n-border);
44
+ border-radius: 2rem;
45
+ display: grid;
46
+ grid-template-columns: 320px 1fr;
47
+ overflow: hidden;
48
+ box-shadow: 0 20px 40px var(--n-shadow);
49
+ }
50
+
51
+ @media (max-width: 850px) {
52
+ .tc-main-card {
53
+ grid-template-columns: 1fr;
54
+ }
55
+ }
56
+
57
+ .tc-calc-sidebar {
58
+ background: var(--n-primary);
59
+ color: var(--n-primary-on);
60
+ padding: 2rem;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 2rem;
64
+ }
65
+
66
+ .tc-sidebar-header {
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ }
71
+
72
+ .tc-sidebar-header h3 {
73
+ font-size: 1.1rem;
74
+ margin: 0;
75
+ font-weight: 700;
76
+ text-transform: uppercase;
77
+ letter-spacing: 0.05em;
78
+ opacity: 0.9;
79
+ }
80
+
81
+ #invert-btn {
82
+ background: rgba(255, 255, 255, 0.1);
83
+ border: none;
84
+ color: var(--n-primary-on);
85
+ width: 36px;
86
+ height: 36px;
87
+ border-radius: 50%;
88
+ cursor: pointer;
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ transition: background 0.2s;
93
+ }
94
+
95
+ #invert-btn:hover {
96
+ background: rgba(255, 255, 255, 0.2);
97
+ }
98
+
99
+ .tc-input-set {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 1.5rem;
103
+ }
104
+
105
+ .tc-input-block {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 0.5rem;
109
+ }
110
+
111
+ .tc-input-block label {
112
+ font-size: 0.85rem;
113
+ font-weight: 700;
114
+ color: var(--n-primary-on);
115
+ opacity: 0.85;
116
+ }
117
+
118
+ .tc-input-block .row {
119
+ display: flex;
120
+ gap: 0.5rem;
121
+ }
122
+
123
+ .tide-calculator input[type='time'],
124
+ .tide-calculator input[type='number'] {
125
+ background: rgba(255, 255, 255, 0.1);
126
+ border: 1px solid rgba(255, 255, 255, 0.2);
127
+ border-radius: 0.8rem;
128
+ padding: 0.8rem;
129
+ color: var(--n-primary-on);
130
+ font-size: 1rem;
131
+ outline: none;
132
+ }
133
+
134
+ .tc-unit-input {
135
+ position: relative;
136
+ display: flex;
137
+ align-items: center;
138
+ }
139
+
140
+ .tc-unit-input input {
141
+ width: 80px;
142
+ padding-right: 25px;
143
+ }
144
+
145
+ .tc-unit-input span {
146
+ position: absolute;
147
+ right: 10px;
148
+ font-size: 0.8rem;
149
+ opacity: 0.5;
150
+ }
151
+
152
+ .tc-featured label {
153
+ color: var(--n-primary-on);
154
+ opacity: 1;
155
+ font-size: 0.95rem;
156
+ }
157
+
158
+ .tide-calculator input[type='time'].large-time {
159
+ background: var(--n-primary-on);
160
+ color: var(--n-primary);
161
+ font-size: 1.8rem;
162
+ font-weight: 800;
163
+ text-align: center;
164
+ }
165
+
166
+ .tc-calc-content {
167
+ padding: 2.5rem;
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 2rem;
171
+ background: var(--n-bg-muted);
172
+ }
173
+
174
+ .tc-top-row {
175
+ display: flex;
176
+ justify-content: space-between;
177
+ align-items: flex-start;
178
+ }
179
+
180
+ .tc-result-display {
181
+ display: flex;
182
+ flex-direction: column;
183
+ }
184
+
185
+ .tc-result-display .label {
186
+ font-size: 0.85rem;
187
+ font-weight: 600;
188
+ color: var(--n-text-muted);
189
+ text-transform: uppercase;
190
+ letter-spacing: 0.05em;
191
+ }
192
+
193
+ .tc-value-box {
194
+ display: flex;
195
+ align-items: baseline;
196
+ gap: 0.5rem;
197
+ margin: 0.5rem 0;
198
+ }
199
+
200
+ #target-value {
201
+ font-size: 4rem;
202
+ font-weight: 800;
203
+ color: var(--n-accent);
204
+ line-height: 1;
205
+ }
206
+
207
+ .tc-value-box small {
208
+ font-size: 1.2rem;
209
+ font-weight: 600;
210
+ color: var(--n-text-dim);
211
+ }
212
+
213
+ .tc-status-indicator {
214
+ padding: 0.4rem 1rem;
215
+ border-radius: 2rem;
216
+ font-size: 0.85rem;
217
+ font-weight: 700;
218
+ display: inline-block;
219
+ width: fit-content;
220
+ background: var(--n-bg-muted);
221
+ color: var(--n-text-dim);
222
+ }
223
+
224
+ .tc-status-indicator.up {
225
+ background: color-mix(in srgb, var(--n-success), transparent 80%);
226
+ color: var(--n-success);
227
+ }
228
+
229
+ .tc-status-indicator.down {
230
+ background: color-mix(in srgb, var(--n-error), transparent 80%);
231
+ color: var(--n-error);
232
+ }
233
+
234
+ .tc-summary-info {
235
+ display: flex;
236
+ gap: 1.5rem;
237
+ }
238
+
239
+ .tc-stat {
240
+ display: flex;
241
+ flex-direction: column;
242
+ text-align: right;
243
+ }
244
+
245
+ .tc-s-label {
246
+ font-size: 0.75rem;
247
+ font-weight: 600;
248
+ color: var(--n-text-muted);
249
+ text-transform: uppercase;
250
+ }
251
+
252
+ .tc-stat span:last-child {
253
+ font-size: 1.1rem;
254
+ font-weight: 700;
255
+ color: var(--n-text);
256
+ }
257
+
258
+ .tc-chart-area {
259
+ background: var(--n-bg);
260
+ border: 1px solid var(--n-border);
261
+ border-radius: 1.5rem;
262
+ padding: 1.5rem;
263
+ height: 250px;
264
+ position: relative;
265
+ }
266
+
267
+ .tc-table-card {
268
+ background: var(--n-bg);
269
+ border: 1px solid var(--n-border);
270
+ border-radius: 1.5rem;
271
+ padding: 1.5rem;
272
+ }
273
+
274
+ .tc-table-card h4 {
275
+ margin: 0 0 1rem;
276
+ font-size: 1rem;
277
+ color: var(--n-text-muted);
278
+ }
279
+
280
+ .tc-table-wrapper {
281
+ overflow-x: auto;
282
+ }
283
+
284
+ .tide-calculator table {
285
+ width: 100%;
286
+ border-collapse: collapse;
287
+ }
288
+
289
+ .tide-calculator th {
290
+ text-align: left;
291
+ padding: 1rem;
292
+ font-size: 0.8rem;
293
+ color: var(--n-text-muted);
294
+ text-transform: uppercase;
295
+ border-bottom: 2px solid var(--n-bg-muted);
296
+ }
297
+
298
+ .tide-calculator td {
299
+ padding: 1rem;
300
+ font-size: 0.95rem;
301
+ color: var(--n-text-muted);
302
+ border-bottom: 1px solid var(--n-bg-muted);
303
+ }
304
+
305
+ @media (max-width: 640px) {
306
+ .tc-top-row {
307
+ flex-direction: column;
308
+ gap: 1.5rem;
309
+ }
310
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { underKeel } 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 underKeel.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} title={content.ui.bibliographyTitle} />}
@@ -0,0 +1,240 @@
1
+ ---
2
+ import './style.css';
3
+ import type { UnderKeelUI } from './index';
4
+
5
+ interface Props {
6
+ ui: UnderKeelUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <section id="under-keel-calculator" class="under-keel-calculator" data-ui={JSON.stringify(ui)}>
13
+ <div class="ukc-main-card">
14
+ <div class="ukc-calc-sidebar">
15
+ <div class="ukc-sidebar-header">
16
+ <h3>{ui.parametersLabel}</h3>
17
+ </div>
18
+
19
+ <div class="ukc-input-set">
20
+ <div class="ukc-input-block">
21
+ <label>{ui.boatDraftLabel}</label>
22
+ <div class="ukc-unit-input">
23
+ <input type="number" id="boat-draft" value="2.0" step="0.1" />
24
+ <span>m</span>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="ukc-input-block">
29
+ <label>{ui.chartDepthLabel}</label>
30
+ <div class="ukc-unit-input">
31
+ <input type="number" id="chart-depth" value="1.5" step="0.1" />
32
+ <span>m</span>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="ukc-input-block">
37
+ <label>{ui.safetyMarginLabel}</label>
38
+ <div class="ukc-unit-input">
39
+ <input type="number" id="safety-margin" value="0.5" step="0.1" />
40
+ <span>m</span>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="ukc-hr-divider"></div>
45
+
46
+ <div class="ukc-input-block">
47
+ <label>{ui.highTideLabel}</label>
48
+ <div class="ukc-row">
49
+ <input type="time" id="high-tide-time" value="12:00" />
50
+ <input type="number" id="high-tide-height" value="3.5" step="0.1" class="ukc-mini-num" />
51
+ </div>
52
+ </div>
53
+
54
+ <div class="ukc-input-block">
55
+ <label>{ui.lowTideLabel}</label>
56
+ <div class="ukc-row">
57
+ <input type="time" id="low-tide-time" value="06:00" />
58
+ <input type="number" id="low-tide-height" value="0.5" step="0.1" class="ukc-mini-num" />
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="ukc-calc-content">
65
+ <div class="ukc-top-row">
66
+ <div class="ukc-result-display">
67
+ <span class="label">{ui.passWindowLabel}</span>
68
+ <div class="ukc-time-range">
69
+ <span id="pass-start">--:--</span>
70
+ <div class="ukc-arrow-sep">
71
+ <svg viewBox="0 0 24 24" width="24" height="24">
72
+ <path fill="currentColor" d="M12,4L10.59,5.41L16.17,11H4V13H16.17L10.59,18.59L12,20L20,12L12,4Z"></path>
73
+ </svg>
74
+ </div>
75
+ <span id="pass-end">--:--</span>
76
+ </div>
77
+ <span id="pass-status" class="ukc-status-indicator">--</span>
78
+ </div>
79
+
80
+ <div class="ukc-data-group">
81
+ <div class="ukc-stat-pill">
82
+ <span>{ui.neededLabel}</span>
83
+ <strong id="needed-depth">--</strong>
84
+ </div>
85
+ <div class="ukc-stat-pill">
86
+ <span>{ui.tideRequiredLabel}</span>
87
+ <strong id="needed-tide">--</strong>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="ukc-visual-container">
93
+ <div class="ukc-ocean-frame">
94
+ <div class="ukc-surface-line"></div>
95
+ <div class="ukc-ship-silhouette" id="viz-boat">
96
+ <svg viewBox="0 0 100 40" preserveAspectRatio="none">
97
+ <path fill="currentColor" d="M10,20 L90,20 L80,35 L20,35 Z"></path>
98
+ </svg>
99
+ </div>
100
+ <div class="ukc-water-volume" id="viz-water"></div>
101
+ <div class="ukc-sand-bottom">
102
+ <span>{ui.bottomLabel}</span>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </section>
109
+
110
+ <script>
111
+ const section = document.getElementById('under-keel-calculator') as HTMLElement;
112
+ const ui = JSON.parse(section.dataset.ui as string);
113
+
114
+ interface CycleData {
115
+ flooding: boolean; sT: number; dur: number;
116
+ sH: number; amp: number; mult: number; tH: number;
117
+ }
118
+
119
+ class UnderKeelCalculator {
120
+ private draftIn: HTMLInputElement;
121
+ private depthIn: HTMLInputElement;
122
+ private marginIn: HTMLInputElement;
123
+ private highTIn: HTMLInputElement;
124
+ private highHIn: HTMLInputElement;
125
+ private lowTIn: HTMLInputElement;
126
+ private lowHIn: HTMLInputElement;
127
+ private statusInd: HTMLElement;
128
+ private startSpan: HTMLElement;
129
+ private endSpan: HTMLElement;
130
+ private neededD: HTMLElement;
131
+ private neededT: HTMLElement;
132
+ private vizWater: HTMLElement;
133
+ private vizBoat: HTMLElement;
134
+
135
+ constructor() {
136
+ this.draftIn = document.getElementById('boat-draft') as HTMLInputElement;
137
+ this.depthIn = document.getElementById('chart-depth') as HTMLInputElement;
138
+ this.marginIn = document.getElementById('safety-margin') as HTMLInputElement;
139
+ this.highTIn = document.getElementById('high-tide-time') as HTMLInputElement;
140
+ this.highHIn = document.getElementById('high-tide-height') as HTMLInputElement;
141
+ this.lowTIn = document.getElementById('low-tide-time') as HTMLInputElement;
142
+ this.lowHIn = document.getElementById('low-tide-height') as HTMLInputElement;
143
+ this.statusInd = document.getElementById('pass-status') as HTMLElement;
144
+ this.startSpan = document.getElementById('pass-start') as HTMLElement;
145
+ this.endSpan = document.getElementById('pass-end') as HTMLElement;
146
+ this.neededD = document.getElementById('needed-depth') as HTMLElement;
147
+ this.neededT = document.getElementById('needed-tide') as HTMLElement;
148
+ this.vizWater = document.getElementById('viz-water') as HTMLElement;
149
+ this.vizBoat = document.getElementById('viz-boat') as HTMLElement;
150
+ this.init();
151
+ }
152
+
153
+ private init() {
154
+ const inputs = [this.draftIn, this.depthIn, this.marginIn, this.highTIn, this.highHIn, this.lowTIn, this.lowHIn];
155
+ inputs.forEach((i) => i.addEventListener('input', () => this.calculate()));
156
+ this.calculate();
157
+ }
158
+
159
+ private tToM(s: string) {
160
+ const [h, m] = s.split(':').map(Number);
161
+ return (h ?? 0) * 60 + (m ?? 0);
162
+ }
163
+
164
+ private mToT(m: number) {
165
+ let mins = Math.round(m) % 1440;
166
+ if (mins < 0) mins += 1440;
167
+ return `${String(Math.floor(mins / 60)).padStart(2, '0')}:${String(mins % 60).padStart(2, '0')}`;
168
+ }
169
+
170
+ private getCycleData(tH: number, hH: number, tL: number, hL: number): CycleData {
171
+ const flooding = tH > tL;
172
+ return {
173
+ flooding,
174
+ sT: flooding ? tL : tH,
175
+ dur: Math.abs(tH - tL),
176
+ sH: flooding ? hL : hH,
177
+ amp: Math.abs(hH - hL),
178
+ mult: flooding ? 1 : -1,
179
+ tH,
180
+ };
181
+ }
182
+
183
+ private findOffset(target: number, dur: number) {
184
+ const doz = [1 / 12, 2 / 12, 3 / 12, 3 / 12, 2 / 12, 1 / 12];
185
+ const step = dur / 6;
186
+ let cum = 0;
187
+ for (let i = 0; i < 6; i++) {
188
+ const d = doz[i] ?? 0;
189
+ if (cum + d >= target) return (i + (target - cum) / d) * step;
190
+ cum += d;
191
+ }
192
+ return dur;
193
+ }
194
+
195
+ private updateUI(txt: string, s: string, e: string, cls: string) {
196
+ this.statusInd.textContent = txt;
197
+ this.statusInd.className = `ukc-status-indicator ${cls}`;
198
+ this.startSpan.textContent = s;
199
+ this.endSpan.textContent = e;
200
+ }
201
+
202
+ private resolveStatus(tideReq: number, hH: number, hL: number, cycle: CycleData) {
203
+ const { flooding, sT, dur, sH, amp, mult, tH } = cycle;
204
+ if (tideReq > hH) { this.updateUI(ui.statusNeverLabel, '--:--', '--:--', 'danger'); return; }
205
+ if (tideReq <= hL) { this.updateUI(ui.statusAlwaysLabel, this.mToT(sT), this.mToT(sT + dur), 'success'); return; }
206
+ const tOff = this.findOffset((tideReq - sH) / (mult * amp), dur);
207
+ const pT = sT + tOff;
208
+ if (flooding) { this.updateUI(ui.statusFromLabel, this.mToT(pT), this.mToT(tH + (tH - pT)), 'warning'); return; }
209
+ this.updateUI(ui.statusUntilLabel, this.mToT(tH - (pT - tH)), this.mToT(pT), 'warning');
210
+ }
211
+
212
+ private updateViz(needed: number, sd: number, hH: number, dr: number) {
213
+ const totalWater = sd + hH;
214
+ const maxVal = Math.max(needed, totalWater, 5) + 2;
215
+ const floorPct = 15;
216
+ const waterPct = floorPct + (totalWater / maxVal) * (100 - floorPct);
217
+ const boatPct = Math.max(floorPct, Math.min(95, floorPct + ((totalWater - dr) / maxVal) * (100 - floorPct)));
218
+ this.vizWater.style.height = `${waterPct}%`;
219
+ this.vizBoat.style.bottom = `${boatPct}%`;
220
+ }
221
+
222
+ private calculate() {
223
+ const dr = parseFloat(this.draftIn.value);
224
+ const sd = parseFloat(this.depthIn.value);
225
+ const mg = parseFloat(this.marginIn.value);
226
+ const hH = parseFloat(this.highHIn.value);
227
+ const hL = parseFloat(this.lowHIn.value);
228
+ if (isNaN(dr) || isNaN(sd) || isNaN(mg) || isNaN(hH)) return;
229
+ const needed = dr + mg;
230
+ const tideReq = Math.max(0, needed - sd);
231
+ this.neededD.textContent = `${needed.toFixed(1)}m`;
232
+ this.neededT.textContent = `${tideReq.toFixed(1)}m`;
233
+ const cycle = this.getCycleData(this.tToM(this.highTIn.value), hH, this.tToM(this.lowTIn.value), hL);
234
+ this.resolveStatus(tideReq, hH, hL, cycle);
235
+ this.updateViz(needed, sd, hH, dr);
236
+ }
237
+ }
238
+
239
+ new UnderKeelCalculator();
240
+ </script>