@jjlmoya/utils-nautical 1.17.0 → 1.18.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.
- package/package.json +4 -2
- package/src/data.ts +0 -6
- package/src/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tool/endurance/component.astro +10 -10
- package/src/tool/nauticalConverter/component.astro +15 -15
- package/src/tool/sailArea/component.astro +13 -13
- package/src/tool/tideCalculator/component.astro +5 -5
- package/src/tool/underKeel/component.astro +7 -7
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-nautical",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./data": "./src/data.ts",
|
|
10
|
-
"./entries": "./src/entries.ts"
|
|
10
|
+
"./entries": "./src/entries.ts",
|
|
11
|
+
"./runtime/*": "./src/tool/*/index.ts",
|
|
12
|
+
"./category-seo": "./src/category/seo.astro"
|
|
11
13
|
},
|
|
12
14
|
"files": [
|
|
13
15
|
"src",
|
package/src/data.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
export { nauticalCategory } from './category';
|
|
2
2
|
export type { CategoryLocaleContent } from './category';
|
|
3
3
|
|
|
4
|
-
export { tideCalculator } from './tool/tideCalculator';
|
|
5
|
-
export { underKeel } from './tool/underKeel';
|
|
6
|
-
export { nauticalConverter } from './tool/nauticalConverter';
|
|
7
|
-
export { sailArea } from './tool/sailArea';
|
|
8
|
-
export { speedConverter } from './tool/speedConverter';
|
|
9
|
-
export { endurance } from './tool/endurance';
|
|
10
4
|
|
|
11
5
|
export type { TideCalculatorUI, TideCalculatorLocaleContent } from './tool/tideCalculator';
|
|
12
6
|
export type { UnderKeelUI, UnderKeelLocaleContent } from './tool/underKeel';
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
import { ALL_TOOLS } from '../tools';
|
|
5
|
+
import type { SEOSection, ToolLocaleContent } from '../types';
|
|
6
|
+
|
|
7
|
+
const srcDir = join(process.cwd(), 'src');
|
|
8
|
+
const toolDir = join(srcDir, 'tool');
|
|
9
|
+
const geometryReads = [
|
|
10
|
+
'offsetWidth',
|
|
11
|
+
'offsetHeight',
|
|
12
|
+
'offsetTop',
|
|
13
|
+
'offsetLeft',
|
|
14
|
+
'clientWidth',
|
|
15
|
+
'clientHeight',
|
|
16
|
+
'clientTop',
|
|
17
|
+
'clientLeft',
|
|
18
|
+
'scrollWidth',
|
|
19
|
+
'scrollHeight',
|
|
20
|
+
'scrollTop',
|
|
21
|
+
'scrollLeft',
|
|
22
|
+
'getBoundingClientRect',
|
|
23
|
+
'getClientRects',
|
|
24
|
+
'computedStyle',
|
|
25
|
+
'getComputedStyle',
|
|
26
|
+
];
|
|
27
|
+
const domWrites = [
|
|
28
|
+
'.style.',
|
|
29
|
+
'.classList.add',
|
|
30
|
+
'.classList.remove',
|
|
31
|
+
'.classList.toggle',
|
|
32
|
+
'.appendChild',
|
|
33
|
+
'.insertBefore',
|
|
34
|
+
'.prepend',
|
|
35
|
+
'.append',
|
|
36
|
+
'.remove',
|
|
37
|
+
'.innerHTML',
|
|
38
|
+
'.textContent',
|
|
39
|
+
'.setAttribute',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function findFiles(dir: string, extensions: string[]): string[] {
|
|
43
|
+
const files: string[] = [];
|
|
44
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
45
|
+
const fullPath = join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) files.push(...findFiles(fullPath, extensions));
|
|
47
|
+
else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(fullPath);
|
|
48
|
+
}
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function relativePath(file: string): string {
|
|
53
|
+
return relative(process.cwd(), file).replace(/\\/g, '/');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findFormControls(content: string, tagName: 'input' | 'select'): RegExpMatchArray[] {
|
|
57
|
+
return Array.from(content.matchAll(new RegExp(`<${tagName}\\b[^>]*>`, 'gi')));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function attrValue(tag: string, attr: string): string | null {
|
|
61
|
+
const match = tag.match(new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|\\{([^}]+)\\})`, 'i'));
|
|
62
|
+
return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function booleanAttr(tag: string, attr: string): boolean {
|
|
66
|
+
return new RegExp(`\\b${attr}\\b`, 'i').test(tag);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function controlStartIndex(content: string, tag: RegExpMatchArray): number {
|
|
70
|
+
return tag.index ?? content.indexOf(tag[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasWrappingLabel(content: string, tag: RegExpMatchArray): boolean {
|
|
74
|
+
const index = controlStartIndex(content, tag);
|
|
75
|
+
const before = content.slice(0, index);
|
|
76
|
+
const labelOpen = before.lastIndexOf('<label');
|
|
77
|
+
const labelClose = before.lastIndexOf('</label>');
|
|
78
|
+
const nextLabelClose = content.indexOf('</label>', index + tag[0].length);
|
|
79
|
+
return labelOpen > labelClose && nextLabelClose !== -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasAccessibleName(content: string, tag: RegExpMatchArray): boolean {
|
|
83
|
+
const source = tag[0];
|
|
84
|
+
if (attrValue(source, 'aria-label')) return true;
|
|
85
|
+
if (attrValue(source, 'aria-labelledby')) return true;
|
|
86
|
+
const id = attrValue(source, 'id');
|
|
87
|
+
if (id && hasExplicitLabel(content, id)) return true;
|
|
88
|
+
return hasWrappingLabel(content, tag);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isVisuallyHiddenFileInput(tag: string): boolean {
|
|
92
|
+
const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
|
|
93
|
+
const attributes = `${attrValue(tag, 'style') ?? ''} ${attrValue(tag, 'class') ?? ''}`.toLowerCase();
|
|
94
|
+
const hiddenPatterns = ['display:none', 'display: none', 'file-input'];
|
|
95
|
+
return type === 'file' && hiddenPatterns.some((pattern) => attributes.includes(pattern));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isIgnoredInput(tag: string): boolean {
|
|
99
|
+
const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
|
|
100
|
+
return ['hidden', 'button', 'submit', 'reset'].includes(type) || booleanAttr(tag, 'aria-hidden') || isVisuallyHiddenFileInput(tag);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function controlFailures(content: string, tagName: 'input' | 'select'): string[] {
|
|
104
|
+
return findFormControls(content, tagName)
|
|
105
|
+
.filter((tag) => tagName !== 'input' || !isIgnoredInput(tag[0]))
|
|
106
|
+
.filter((tag) => !hasAccessibleName(content, tag))
|
|
107
|
+
.map((tag) => tag[0]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function explicitLabelMessage(tagName: string, path: string, failures: string[]): string {
|
|
111
|
+
return `${tagName} controls without label, wrapping label, aria-label or aria-labelledby in ${path}:\n${failures.join('\n')}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasExplicitLabel(content: string, id: string): boolean {
|
|
115
|
+
const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
+
return (
|
|
117
|
+
new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${escapedId}["'][^>]*>`, 'i').test(content)
|
|
118
|
+
|| new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*\\{${escapedId}\\}[^>]*>`, 'i').test(content)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function headingLevels(sections: SEOSection[]): number[] {
|
|
123
|
+
return sections
|
|
124
|
+
.filter((section) => section.type === 'title')
|
|
125
|
+
.map((section) => Number('level' in section ? section.level : 0))
|
|
126
|
+
.filter((level) => Number.isInteger(level) && level > 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findHeadingLevelJumps(levels: number[]): string[] {
|
|
130
|
+
const failures: string[] = [];
|
|
131
|
+
levels.forEach((level, index) => {
|
|
132
|
+
const previous = index === 0 ? 1 : levels[index - 1];
|
|
133
|
+
if (previous && level > previous + 1) {
|
|
134
|
+
failures.push(`h${previous} -> h${level}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return failures;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasDomWriteBeforeGeometryRead(content: string): boolean {
|
|
141
|
+
const normalized = content.replace(/\s+/g, ' ');
|
|
142
|
+
return domWrites.some((write) => {
|
|
143
|
+
const writeIndex = normalized.indexOf(write);
|
|
144
|
+
if (writeIndex === -1) return false;
|
|
145
|
+
return geometryReads.some((read) => normalized.indexOf(read, writeIndex + write.length) !== -1);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
describe('PageSpeed best-practice guards', () => {
|
|
150
|
+
const astroToolFiles = findFiles(toolDir, ['.astro']);
|
|
151
|
+
const scriptFiles = findFiles(toolDir, ['.astro', '.ts', '.js']);
|
|
152
|
+
|
|
153
|
+
astroToolFiles.forEach((file) => {
|
|
154
|
+
const displayPath = relativePath(file);
|
|
155
|
+
|
|
156
|
+
it(`${displayPath} labels every input with an explicit label`, () => {
|
|
157
|
+
const content = readFileSync(file, 'utf-8');
|
|
158
|
+
const failures = controlFailures(content, 'input');
|
|
159
|
+
|
|
160
|
+
expect(failures, explicitLabelMessage('Input', displayPath, failures)).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it(`${displayPath} labels every select with an explicit label`, () => {
|
|
164
|
+
const content = readFileSync(file, 'utf-8');
|
|
165
|
+
const failures = controlFailures(content, 'select');
|
|
166
|
+
|
|
167
|
+
expect(failures, explicitLabelMessage('Select', displayPath, failures)).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
ALL_TOOLS.forEach((tool) => {
|
|
172
|
+
Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
|
|
173
|
+
it(`${tool.entry.id}/${locale} keeps SEO headings sequential`, async () => {
|
|
174
|
+
if (!loader) return;
|
|
175
|
+
const content = (await loader()) as ToolLocaleContent;
|
|
176
|
+
const levels = headingLevels(content.seo);
|
|
177
|
+
const failures = findHeadingLevelJumps(levels);
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
failures,
|
|
181
|
+
`SEO headings in ${tool.entry.id}/${locale} skip levels: ${failures.join(', ')}`,
|
|
182
|
+
).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
scriptFiles.forEach((file) => {
|
|
188
|
+
const displayPath = relativePath(file);
|
|
189
|
+
|
|
190
|
+
it(`${displayPath} avoids static forced-reflow patterns`, () => {
|
|
191
|
+
const content = readFileSync(file, 'utf-8');
|
|
192
|
+
expect(
|
|
193
|
+
hasDomWriteBeforeGeometryRead(content),
|
|
194
|
+
`${displayPath} appears to read layout geometry after DOM/style mutations. Split writes and reads across frames or measure before mutating.`,
|
|
195
|
+
).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -12,10 +12,10 @@ const { ui } = Astro.props;
|
|
|
12
12
|
<div class="ec-main-container">
|
|
13
13
|
<aside class="ec-sidebar-inputs">
|
|
14
14
|
<div class="ec-compact-group">
|
|
15
|
-
<label>{ui.tankCapacityLabel}</
|
|
15
|
+
<span class="ec-group-label">{ui.tankCapacityLabel}</span>
|
|
16
16
|
<div class="ec-dual-row">
|
|
17
17
|
<div>
|
|
18
|
-
<
|
|
18
|
+
<label for="tank1" class="ec-sub-label">{ui.mainTankLabel}</label>
|
|
19
19
|
<div class="ec-field-wrapper">
|
|
20
20
|
<span class="ec-field-icon icon-fuel"></span>
|
|
21
21
|
<input type="number" id="tank1" class="ec-compact-input" value="200" min="0" step="10" />
|
|
@@ -23,7 +23,7 @@ const { ui } = Astro.props;
|
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|
|
25
25
|
<div>
|
|
26
|
-
<
|
|
26
|
+
<label for="tank2" class="ec-sub-label">{ui.auxTankLabel}</label>
|
|
27
27
|
<div class="ec-field-wrapper">
|
|
28
28
|
<span class="ec-field-icon icon-fuel"></span>
|
|
29
29
|
<input type="number" id="tank2" class="ec-compact-input" value="0" min="0" step="10" />
|
|
@@ -34,7 +34,7 @@ const { ui } = Astro.props;
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
<div class="ec-compact-group">
|
|
37
|
-
<label>{ui.currentFuelLabel}</label>
|
|
37
|
+
<label for="f-current">{ui.currentFuelLabel}</label>
|
|
38
38
|
<div class="ec-field-wrapper">
|
|
39
39
|
<span class="ec-field-icon icon-bucket"></span>
|
|
40
40
|
<input type="number" id="f-current" class="ec-compact-input" value="150" min="0" step="5" />
|
|
@@ -43,7 +43,7 @@ const { ui } = Astro.props;
|
|
|
43
43
|
</div>
|
|
44
44
|
|
|
45
45
|
<div class="ec-compact-group">
|
|
46
|
-
<label>{ui.seaConditionsLabel}</label>
|
|
46
|
+
<label for="sea-s">{ui.seaConditionsLabel}</label>
|
|
47
47
|
<div class="ec-field-wrapper">
|
|
48
48
|
<span class="ec-field-icon icon-wave"></span>
|
|
49
49
|
<select id="sea-s" class="ec-compact-select">
|
|
@@ -56,7 +56,7 @@ const { ui } = Astro.props;
|
|
|
56
56
|
</div>
|
|
57
57
|
|
|
58
58
|
<div class="ec-compact-group">
|
|
59
|
-
<label>{ui.consumptionLabel}</label>
|
|
59
|
+
<label for="f-cons">{ui.consumptionLabel}</label>
|
|
60
60
|
<div class="ec-field-wrapper">
|
|
61
61
|
<span class="ec-field-icon icon-fire"></span>
|
|
62
62
|
<input type="number" id="f-cons" class="ec-compact-input" value="25" min="0.1" step="1" />
|
|
@@ -65,7 +65,7 @@ const { ui } = Astro.props;
|
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
67
|
<div class="ec-compact-group">
|
|
68
|
-
<label>{ui.cruiseSpeedLabel}</label>
|
|
68
|
+
<label for="v-cruiser">{ui.cruiseSpeedLabel}</label>
|
|
69
69
|
<div class="ec-field-wrapper">
|
|
70
70
|
<span class="ec-field-icon icon-wind"></span>
|
|
71
71
|
<input type="number" id="v-cruiser" class="ec-compact-input" value="8" min="0.1" step="0.5" />
|
|
@@ -74,7 +74,7 @@ const { ui } = Astro.props;
|
|
|
74
74
|
</div>
|
|
75
75
|
|
|
76
76
|
<div class="ec-compact-group">
|
|
77
|
-
<label>{ui.reserveLabel}</label>
|
|
77
|
+
<label for="f-res">{ui.reserveLabel}</label>
|
|
78
78
|
<div class="ec-field-wrapper">
|
|
79
79
|
<span class="ec-field-icon icon-shield"></span>
|
|
80
80
|
<input type="number" id="f-res" class="ec-compact-input" value="20" min="0" max="50" step="5" />
|
|
@@ -83,7 +83,7 @@ const { ui } = Astro.props;
|
|
|
83
83
|
</div>
|
|
84
84
|
|
|
85
85
|
<div class="ec-compact-group">
|
|
86
|
-
<label>{ui.fuelPriceLabel}</label>
|
|
86
|
+
<label for="f-price">{ui.fuelPriceLabel}</label>
|
|
87
87
|
<div class="ec-field-wrapper">
|
|
88
88
|
<span class="ec-field-icon">€</span>
|
|
89
89
|
<input type="number" id="f-price" class="ec-compact-input" value="1.8" min="0" step="0.05" />
|
|
@@ -137,7 +137,7 @@ const { ui } = Astro.props;
|
|
|
137
137
|
<div class="ec-inverter-header">{ui.inverseCalcLabel}</div>
|
|
138
138
|
<div class="ec-inverter-row">
|
|
139
139
|
<div>
|
|
140
|
-
<
|
|
140
|
+
<label for="inv-dist" class="ec-inverter-label">{ui.desiredDistLabel}</label>
|
|
141
141
|
<div class="ec-field-wrapper">
|
|
142
142
|
<input type="number" id="inv-dist" class="ec-compact-input" value="50" min="0" step="5" />
|
|
143
143
|
<span class="ec-unit-tag">MN</span>
|
|
@@ -75,7 +75,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
75
75
|
<div class="nc-converter-card">
|
|
76
76
|
<div class="nc-input-grid">
|
|
77
77
|
<div class="nc-input-group">
|
|
78
|
-
<label>{ui.nmLabel}</label>
|
|
78
|
+
<label for="dist-nm">{ui.nmLabel}</label>
|
|
79
79
|
<div class="nc-input-wrapper">
|
|
80
80
|
<input class="nc-elegant-input" type="number" id="dist-nm" step="any" placeholder="0" />
|
|
81
81
|
<button class="nc-copy-btn" data-copy="dist-nm" title={ui.copyLabel}>
|
|
@@ -84,7 +84,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
84
84
|
</div>
|
|
85
85
|
</div>
|
|
86
86
|
<div class="nc-input-group">
|
|
87
|
-
<label>{ui.kmLabel}</label>
|
|
87
|
+
<label for="dist-km">{ui.kmLabel}</label>
|
|
88
88
|
<div class="nc-input-wrapper">
|
|
89
89
|
<input class="nc-elegant-input" type="number" id="dist-km" step="any" placeholder="0" />
|
|
90
90
|
<button class="nc-copy-btn" data-copy="dist-km" title={ui.copyLabel}>
|
|
@@ -93,7 +93,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
95
|
<div class="nc-input-group">
|
|
96
|
-
<label>{ui.miLabel}</label>
|
|
96
|
+
<label for="dist-mi">{ui.miLabel}</label>
|
|
97
97
|
<div class="nc-input-wrapper">
|
|
98
98
|
<input class="nc-elegant-input" type="number" id="dist-mi" step="any" placeholder="0" />
|
|
99
99
|
<button class="nc-copy-btn" data-copy="dist-mi" title={ui.copyLabel}>
|
|
@@ -102,7 +102,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
102
102
|
</div>
|
|
103
103
|
</div>
|
|
104
104
|
<div class="nc-input-group">
|
|
105
|
-
<label>{ui.cableLabel}</label>
|
|
105
|
+
<label for="dist-cable">{ui.cableLabel}</label>
|
|
106
106
|
<div class="nc-input-wrapper">
|
|
107
107
|
<input class="nc-elegant-input" type="number" id="dist-cable" step="any" placeholder="0" />
|
|
108
108
|
<button class="nc-copy-btn" data-copy="dist-cable" title={ui.copyLabel}>
|
|
@@ -118,7 +118,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
118
118
|
<div class="nc-converter-card">
|
|
119
119
|
<div class="nc-input-grid">
|
|
120
120
|
<div class="nc-input-group">
|
|
121
|
-
<label>{ui.knLabel}</label>
|
|
121
|
+
<label for="speed-kn">{ui.knLabel}</label>
|
|
122
122
|
<div class="nc-input-wrapper">
|
|
123
123
|
<input class="nc-elegant-input" type="number" id="speed-kn" step="any" placeholder="0" />
|
|
124
124
|
<button class="nc-copy-btn" data-copy="speed-kn" title={ui.copyLabel}>
|
|
@@ -127,7 +127,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
127
127
|
</div>
|
|
128
128
|
</div>
|
|
129
129
|
<div class="nc-input-group">
|
|
130
|
-
<label>{ui.kmhLabel}</label>
|
|
130
|
+
<label for="speed-kmh">{ui.kmhLabel}</label>
|
|
131
131
|
<div class="nc-input-wrapper">
|
|
132
132
|
<input class="nc-elegant-input" type="number" id="speed-kmh" step="any" placeholder="0" />
|
|
133
133
|
<button class="nc-copy-btn" data-copy="speed-kmh" title={ui.copyLabel}>
|
|
@@ -136,7 +136,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
136
136
|
</div>
|
|
137
137
|
</div>
|
|
138
138
|
<div class="nc-input-group">
|
|
139
|
-
<label>{ui.msLabel}</label>
|
|
139
|
+
<label for="speed-ms">{ui.msLabel}</label>
|
|
140
140
|
<div class="nc-input-wrapper">
|
|
141
141
|
<input class="nc-elegant-input" type="number" id="speed-ms" step="any" placeholder="0" />
|
|
142
142
|
<button class="nc-copy-btn" data-copy="speed-ms" title={ui.copyLabel}>
|
|
@@ -145,7 +145,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
145
145
|
</div>
|
|
146
146
|
</div>
|
|
147
147
|
<div class="nc-input-group">
|
|
148
|
-
<label>{ui.mphLabel}</label>
|
|
148
|
+
<label for="speed-mph">{ui.mphLabel}</label>
|
|
149
149
|
<div class="nc-input-wrapper">
|
|
150
150
|
<input class="nc-elegant-input" type="number" id="speed-mph" step="any" placeholder="0" />
|
|
151
151
|
<button class="nc-copy-btn" data-copy="speed-mph" title={ui.copyLabel}>
|
|
@@ -168,7 +168,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
168
168
|
<div class="nc-converter-card">
|
|
169
169
|
<div class="nc-input-grid">
|
|
170
170
|
<div class="nc-input-group">
|
|
171
|
-
<label>{ui.brozaLabel}</label>
|
|
171
|
+
<label for="depth-braza">{ui.brozaLabel}</label>
|
|
172
172
|
<div class="nc-input-wrapper">
|
|
173
173
|
<input class="nc-elegant-input" type="number" id="depth-braza" step="any" placeholder="0" />
|
|
174
174
|
<button class="nc-copy-btn" data-copy="depth-braza" title={ui.copyLabel}>
|
|
@@ -177,7 +177,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
177
177
|
</div>
|
|
178
178
|
</div>
|
|
179
179
|
<div class="nc-input-group">
|
|
180
|
-
<label>{ui.mLabel}</label>
|
|
180
|
+
<label for="depth-m">{ui.mLabel}</label>
|
|
181
181
|
<div class="nc-input-wrapper">
|
|
182
182
|
<input class="nc-elegant-input" type="number" id="depth-m" step="any" placeholder="0" />
|
|
183
183
|
<button class="nc-copy-btn" data-copy="depth-m" title={ui.copyLabel}>
|
|
@@ -186,7 +186,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
186
186
|
</div>
|
|
187
187
|
</div>
|
|
188
188
|
<div class="nc-input-group">
|
|
189
|
-
<label>{ui.ftLabel}</label>
|
|
189
|
+
<label for="depth-ft">{ui.ftLabel}</label>
|
|
190
190
|
<div class="nc-input-wrapper">
|
|
191
191
|
<input class="nc-elegant-input" type="number" id="depth-ft" step="any" placeholder="0" />
|
|
192
192
|
<button class="nc-copy-btn" data-copy="depth-ft" title={ui.copyLabel}>
|
|
@@ -202,7 +202,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
202
202
|
<div class="nc-converter-card">
|
|
203
203
|
<div class="nc-input-grid">
|
|
204
204
|
<div class="nc-input-group">
|
|
205
|
-
<label>{ui.mbarLabel}</label>
|
|
205
|
+
<label for="pres-mbar">{ui.mbarLabel}</label>
|
|
206
206
|
<div class="nc-input-wrapper">
|
|
207
207
|
<input class="nc-elegant-input" type="number" id="pres-mbar" step="any" placeholder="1013" />
|
|
208
208
|
<button class="nc-copy-btn" data-copy="pres-mbar" title={ui.copyLabel}>
|
|
@@ -211,7 +211,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
211
211
|
</div>
|
|
212
212
|
</div>
|
|
213
213
|
<div class="nc-input-group">
|
|
214
|
-
<label>{ui.mmhgLabel}</label>
|
|
214
|
+
<label for="pres-mmhg">{ui.mmhgLabel}</label>
|
|
215
215
|
<div class="nc-input-wrapper">
|
|
216
216
|
<input class="nc-elegant-input" type="number" id="pres-mmhg" step="any" placeholder="760" />
|
|
217
217
|
<button class="nc-copy-btn" data-copy="pres-mmhg" title={ui.copyLabel}>
|
|
@@ -220,7 +220,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
220
220
|
</div>
|
|
221
221
|
</div>
|
|
222
222
|
<div class="nc-input-group">
|
|
223
|
-
<label>{ui.inhgLabel}</label>
|
|
223
|
+
<label for="pres-inhg">{ui.inhgLabel}</label>
|
|
224
224
|
<div class="nc-input-wrapper">
|
|
225
225
|
<input class="nc-elegant-input" type="number" id="pres-inhg" step="any" placeholder="29.92" />
|
|
226
226
|
<button class="nc-copy-btn" data-copy="pres-inhg" title={ui.copyLabel}>
|
|
@@ -229,7 +229,7 @@ const allBeaufort = { es: beaufortEs, en: beaufortEn, fr: beaufortFr };
|
|
|
229
229
|
</div>
|
|
230
230
|
</div>
|
|
231
231
|
<div class="nc-input-group">
|
|
232
|
-
<label>{ui.atmLabel}</label>
|
|
232
|
+
<label for="pres-atm">{ui.atmLabel}</label>
|
|
233
233
|
<div class="nc-input-wrapper">
|
|
234
234
|
<input class="nc-elegant-input" type="number" id="pres-atm" step="any" placeholder="1" />
|
|
235
235
|
<button class="nc-copy-btn" data-copy="pres-atm" title={ui.copyLabel}>
|
|
@@ -20,7 +20,7 @@ const { ui } = Astro.props;
|
|
|
20
20
|
<div class="sa-input-with-unit">
|
|
21
21
|
<input type="number" id="displacement" class="sa-elegant-input" placeholder="8500" />
|
|
22
22
|
<div class="sa-unit-tag">
|
|
23
|
-
<select id="disp-unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
23
|
+
<select id="disp-unit" aria-label="Displacement unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
24
24
|
<option value="kg">KG</option>
|
|
25
25
|
<option value="lbs">LBS</option>
|
|
26
26
|
</select>
|
|
@@ -50,11 +50,11 @@ const { ui } = Astro.props;
|
|
|
50
50
|
<h3>{ui.rigGeometryLabel}</h3>
|
|
51
51
|
<div class="sa-compact-grid">
|
|
52
52
|
<div class="sa-input-row">
|
|
53
|
-
<label>P (m)</label>
|
|
53
|
+
<label for="val-p">P (m)</label>
|
|
54
54
|
<div class="sa-input-with-unit">
|
|
55
55
|
<input type="number" id="val-p" class="sa-elegant-input" placeholder="12.5" />
|
|
56
56
|
<div class="sa-unit-tag">
|
|
57
|
-
<select class="sa-len-unit" data-for="val-p" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
57
|
+
<select class="sa-len-unit" data-for="val-p" aria-label="P length unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
58
58
|
<option value="m">M</option>
|
|
59
59
|
<option value="ft">FT</option>
|
|
60
60
|
</select>
|
|
@@ -62,11 +62,11 @@ const { ui } = Astro.props;
|
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|
|
64
64
|
<div class="sa-input-row">
|
|
65
|
-
<label>E (m)</label>
|
|
65
|
+
<label for="val-e">E (m)</label>
|
|
66
66
|
<div class="sa-input-with-unit">
|
|
67
67
|
<input type="number" id="val-e" class="sa-elegant-input" placeholder="4.2" />
|
|
68
68
|
<div class="sa-unit-tag">
|
|
69
|
-
<select class="sa-len-unit" data-for="val-e" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
69
|
+
<select class="sa-len-unit" data-for="val-e" aria-label="E length unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
70
70
|
<option value="m">M</option>
|
|
71
71
|
<option value="ft">FT</option>
|
|
72
72
|
</select>
|
|
@@ -74,11 +74,11 @@ const { ui } = Astro.props;
|
|
|
74
74
|
</div>
|
|
75
75
|
</div>
|
|
76
76
|
<div class="sa-input-row">
|
|
77
|
-
<label>I (m)</label>
|
|
77
|
+
<label for="val-i">I (m)</label>
|
|
78
78
|
<div class="sa-input-with-unit">
|
|
79
79
|
<input type="number" id="val-i" class="sa-elegant-input" placeholder="14.0" />
|
|
80
80
|
<div class="sa-unit-tag">
|
|
81
|
-
<select class="sa-len-unit" data-for="val-i" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
81
|
+
<select class="sa-len-unit" data-for="val-i" aria-label="I length unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
82
82
|
<option value="m">M</option>
|
|
83
83
|
<option value="ft">FT</option>
|
|
84
84
|
</select>
|
|
@@ -86,11 +86,11 @@ const { ui } = Astro.props;
|
|
|
86
86
|
</div>
|
|
87
87
|
</div>
|
|
88
88
|
<div class="sa-input-row">
|
|
89
|
-
<label>J (m)</label>
|
|
89
|
+
<label for="val-j">J (m)</label>
|
|
90
90
|
<div class="sa-input-with-unit">
|
|
91
91
|
<input type="number" id="val-j" class="sa-elegant-input" placeholder="4.5" />
|
|
92
92
|
<div class="sa-unit-tag">
|
|
93
|
-
<select class="sa-len-unit" data-for="val-j" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
93
|
+
<select class="sa-len-unit" data-for="val-j" aria-label="J length unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
94
94
|
<option value="m">M</option>
|
|
95
95
|
<option value="ft">FT</option>
|
|
96
96
|
</select>
|
|
@@ -101,11 +101,11 @@ const { ui } = Astro.props;
|
|
|
101
101
|
|
|
102
102
|
<div id="mizzen-row" style="display: none; margin-top: 1.5rem;" class="sa-compact-grid">
|
|
103
103
|
<div class="sa-input-row">
|
|
104
|
-
<label>Py (m)</label>
|
|
104
|
+
<label for="val-py">Py (m)</label>
|
|
105
105
|
<div class="sa-input-with-unit">
|
|
106
106
|
<input type="number" id="val-py" class="sa-elegant-input" placeholder="8.5" />
|
|
107
107
|
<div class="sa-unit-tag">
|
|
108
|
-
<select class="sa-len-unit" data-for="val-py" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
108
|
+
<select class="sa-len-unit" data-for="val-py" aria-label="Py length unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
109
109
|
<option value="m">M</option>
|
|
110
110
|
<option value="ft">FT</option>
|
|
111
111
|
</select>
|
|
@@ -113,11 +113,11 @@ const { ui } = Astro.props;
|
|
|
113
113
|
</div>
|
|
114
114
|
</div>
|
|
115
115
|
<div class="sa-input-row">
|
|
116
|
-
<label>Ey (m)</label>
|
|
116
|
+
<label for="val-ey">Ey (m)</label>
|
|
117
117
|
<div class="sa-input-with-unit">
|
|
118
118
|
<input type="number" id="val-ey" class="sa-elegant-input" placeholder="3.0" />
|
|
119
119
|
<div class="sa-unit-tag">
|
|
120
|
-
<select class="sa-len-unit" data-for="val-ey" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
120
|
+
<select class="sa-len-unit" data-for="val-ey" aria-label="Ey length unit" style="background:transparent; border:none; appearance:none; font-weight:900; cursor:pointer;">
|
|
121
121
|
<option value="m">M</option>
|
|
122
122
|
<option value="ft">FT</option>
|
|
123
123
|
</select>
|
|
@@ -20,29 +20,29 @@ const { ui } = Astro.props;
|
|
|
20
20
|
|
|
21
21
|
<div class="tc-input-set">
|
|
22
22
|
<div class="tc-input-block">
|
|
23
|
-
<label>{ui.highTideLabel}</label>
|
|
23
|
+
<label for="high-tide-time">{ui.highTideLabel}</label>
|
|
24
24
|
<div class="row">
|
|
25
25
|
<input type="time" id="high-tide-time" value="12:00" />
|
|
26
26
|
<div class="tc-unit-input">
|
|
27
|
-
<input type="number" id="high-tide-height" value="3.5" step="0.1" />
|
|
27
|
+
<input type="number" id="high-tide-height" value="3.5" step="0.1" aria-label="High tide height" />
|
|
28
28
|
<span>m</span>
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
33
|
<div class="tc-input-block">
|
|
34
|
-
<label>{ui.lowTideLabel}</label>
|
|
34
|
+
<label for="low-tide-time">{ui.lowTideLabel}</label>
|
|
35
35
|
<div class="row">
|
|
36
36
|
<input type="time" id="low-tide-time" value="06:00" />
|
|
37
37
|
<div class="tc-unit-input">
|
|
38
|
-
<input type="number" id="low-tide-height" value="0.5" step="0.1" />
|
|
38
|
+
<input type="number" id="low-tide-height" value="0.5" step="0.1" aria-label="Low tide height" />
|
|
39
39
|
<span>m</span>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
42
|
</div>
|
|
43
43
|
|
|
44
44
|
<div class="tc-input-block tc-featured">
|
|
45
|
-
<label>{ui.targetTimeLabel}</label>
|
|
45
|
+
<label for="target-time">{ui.targetTimeLabel}</label>
|
|
46
46
|
<input type="time" id="target-time" value="09:00" class="large-time" />
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
@@ -17,7 +17,7 @@ const { ui } = Astro.props;
|
|
|
17
17
|
|
|
18
18
|
<div class="ukc-input-set">
|
|
19
19
|
<div class="ukc-input-block">
|
|
20
|
-
<label>{ui.boatDraftLabel}</label>
|
|
20
|
+
<label for="boat-draft">{ui.boatDraftLabel}</label>
|
|
21
21
|
<div class="ukc-unit-input">
|
|
22
22
|
<input type="number" id="boat-draft" value="2.0" step="0.1" />
|
|
23
23
|
<span>m</span>
|
|
@@ -25,7 +25,7 @@ const { ui } = Astro.props;
|
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<div class="ukc-input-block">
|
|
28
|
-
<label>{ui.chartDepthLabel}</label>
|
|
28
|
+
<label for="chart-depth">{ui.chartDepthLabel}</label>
|
|
29
29
|
<div class="ukc-unit-input">
|
|
30
30
|
<input type="number" id="chart-depth" value="1.5" step="0.1" />
|
|
31
31
|
<span>m</span>
|
|
@@ -33,7 +33,7 @@ const { ui } = Astro.props;
|
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
35
|
<div class="ukc-input-block">
|
|
36
|
-
<label>{ui.safetyMarginLabel}</label>
|
|
36
|
+
<label for="safety-margin">{ui.safetyMarginLabel}</label>
|
|
37
37
|
<div class="ukc-unit-input">
|
|
38
38
|
<input type="number" id="safety-margin" value="0.5" step="0.1" />
|
|
39
39
|
<span>m</span>
|
|
@@ -43,18 +43,18 @@ const { ui } = Astro.props;
|
|
|
43
43
|
<div class="ukc-hr-divider"></div>
|
|
44
44
|
|
|
45
45
|
<div class="ukc-input-block">
|
|
46
|
-
<label>{ui.highTideLabel}</label>
|
|
46
|
+
<label for="high-tide-time">{ui.highTideLabel}</label>
|
|
47
47
|
<div class="ukc-row">
|
|
48
48
|
<input type="time" id="high-tide-time" value="12:00" />
|
|
49
|
-
<input type="number" id="high-tide-height" value="3.5" step="0.1" class="ukc-mini-num" />
|
|
49
|
+
<input type="number" id="high-tide-height" value="3.5" step="0.1" class="ukc-mini-num" aria-label="High tide height" />
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
52
52
|
|
|
53
53
|
<div class="ukc-input-block">
|
|
54
|
-
<label>{ui.lowTideLabel}</label>
|
|
54
|
+
<label for="low-tide-time">{ui.lowTideLabel}</label>
|
|
55
55
|
<div class="ukc-row">
|
|
56
56
|
<input type="time" id="low-tide-time" value="06:00" />
|
|
57
|
-
<input type="number" id="low-tide-height" value="0.5" step="0.1" class="ukc-mini-num" />
|
|
57
|
+
<input type="number" id="low-tide-height" value="0.5" step="0.1" class="ukc-mini-num" aria-label="Low tide height" />
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
</div>
|