@jjlmoya/utils-diy 1.18.0 → 1.20.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/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tool/clayCalculator/component.astro +1 -1
- package/src/tool/concreteCalculator/component.astro +12 -10
- package/src/tool/cutOptimizer/component.astro +9 -9
- package/src/tool/drillCalculator/component.astro +4 -4
- package/src/tool/drillSharpener/component.astro +1 -0
- package/src/tool/epoxyCalculator/component.astro +8 -8
- package/src/tool/epoxyCalculator/epoxy-resin-calculator.css +64 -0
- package/src/tool/furnitureFit/component.astro +14 -14
- package/src/tool/mortarCalculator/component.astro +1 -1
- package/src/tool/passepartoutCalculator/component.astro +10 -10
- package/src/tool/passepartoutCalculator/passepartout-calculator.css +77 -0
- package/src/tool/stairCalculator/stair-calculator.css +9 -0
- package/src/tool/twoStrokeMixtureCalculator/component.astro +1 -1
- package/src/tool/voltageDropCalculator/component.astro +4 -4
- package/src/tool/workshopFractionConverter/component.astro +4 -4
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-diy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.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",
|
|
@@ -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
|
+
});
|
|
@@ -28,7 +28,7 @@ const t = (ui ?? {}) as ClayCalculatorUI;
|
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<div class="cc-field">
|
|
31
|
-
<label class="cc-label">{t.labelShrinkage}</label>
|
|
31
|
+
<label class="cc-label" for="cc-shrinkage-slider">{t.labelShrinkage}</label>
|
|
32
32
|
<div class="cc-slider-row">
|
|
33
33
|
<input
|
|
34
34
|
type="range"
|
|
@@ -53,23 +53,23 @@ const t = (ui ?? {}) as ConcreteCalculatorUI;
|
|
|
53
53
|
<div class="cc-dims-panel cc-active" data-cc-mode-panel="dims">
|
|
54
54
|
<div class="cc-dim-inputs">
|
|
55
55
|
<div class="cc-dim-field">
|
|
56
|
-
<label>{t.labelLength}</label>
|
|
56
|
+
<label for="cc-length">{t.labelLength}</label>
|
|
57
57
|
<div class="cc-dim-input-wrap">
|
|
58
|
-
<input type="number" data-cc-length data-cc-dim value="5" step="0.5" />
|
|
58
|
+
<input type="number" id="cc-length" data-cc-length data-cc-dim value="5" step="0.5" />
|
|
59
59
|
<span class="cc-dim-unit">m</span>
|
|
60
60
|
</div>
|
|
61
61
|
</div>
|
|
62
62
|
<div class="cc-dim-field">
|
|
63
|
-
<label>{t.labelWidth}</label>
|
|
63
|
+
<label for="cc-width">{t.labelWidth}</label>
|
|
64
64
|
<div class="cc-dim-input-wrap">
|
|
65
|
-
<input type="number" data-cc-width data-cc-dim value="3" step="0.5" />
|
|
65
|
+
<input type="number" id="cc-width" data-cc-width data-cc-dim value="3" step="0.5" />
|
|
66
66
|
<span class="cc-dim-unit">m</span>
|
|
67
67
|
</div>
|
|
68
68
|
</div>
|
|
69
69
|
<div class="cc-dim-field">
|
|
70
|
-
<label>{t.labelThickness}</label>
|
|
70
|
+
<label for="cc-thickness">{t.labelThickness}</label>
|
|
71
71
|
<div class="cc-dim-input-wrap">
|
|
72
|
-
<input type="number" data-cc-thickness data-cc-dim value="10" step="1" />
|
|
72
|
+
<input type="number" id="cc-thickness" data-cc-thickness data-cc-dim value="10" step="1" />
|
|
73
73
|
<span class="cc-dim-unit">cm</span>
|
|
74
74
|
</div>
|
|
75
75
|
</div>
|
|
@@ -78,10 +78,10 @@ const t = (ui ?? {}) as ConcreteCalculatorUI;
|
|
|
78
78
|
|
|
79
79
|
<div class="cc-direct-panel" data-cc-mode-panel="direct">
|
|
80
80
|
<div class="cc-input-header">
|
|
81
|
-
<label>{t.labelVolume}</label>
|
|
81
|
+
<label for="cc-volume">{t.labelVolume}</label>
|
|
82
82
|
</div>
|
|
83
83
|
<div class="cc-vol-input-wrap">
|
|
84
|
-
<input type="number" data-cc-volume value="1.5" step="0.1" min="0" />
|
|
84
|
+
<input type="number" id="cc-volume" data-cc-volume value="1.5" step="0.1" min="0" />
|
|
85
85
|
<span class="cc-unit-tag">m³</span>
|
|
86
86
|
</div>
|
|
87
87
|
</div>
|
|
@@ -172,13 +172,14 @@ const t = (ui ?? {}) as ConcreteCalculatorUI;
|
|
|
172
172
|
</div>
|
|
173
173
|
<div class="cc-price-inputs-grid">
|
|
174
174
|
<div class="cc-price-input-field">
|
|
175
|
-
<label>
|
|
175
|
+
<label for="cc-price-binder">
|
|
176
176
|
{t.priceSackLabel}
|
|
177
177
|
<span data-cc-binder-type>{t.binderCementLabel}</span> (25kg)
|
|
178
178
|
</label>
|
|
179
179
|
<div class="cc-editable-price-wrap">
|
|
180
180
|
<input
|
|
181
181
|
type="number"
|
|
182
|
+
id="cc-price-binder"
|
|
182
183
|
data-cc-price-binder
|
|
183
184
|
class="cc-editable-price"
|
|
184
185
|
value="5.50"
|
|
@@ -188,10 +189,11 @@ const t = (ui ?? {}) as ConcreteCalculatorUI;
|
|
|
188
189
|
</div>
|
|
189
190
|
</div>
|
|
190
191
|
<div class="cc-price-input-field">
|
|
191
|
-
<label>{t.priceSandLabel}</label>
|
|
192
|
+
<label for="cc-price-sand">{t.priceSandLabel}</label>
|
|
192
193
|
<div class="cc-editable-price-wrap">
|
|
193
194
|
<input
|
|
194
195
|
type="number"
|
|
196
|
+
id="cc-price-sand"
|
|
195
197
|
data-cc-price-sand
|
|
196
198
|
class="cc-editable-price"
|
|
197
199
|
value="35.00"
|
|
@@ -36,16 +36,16 @@ const t = (ui ?? {}) as CutOptimizerUI;
|
|
|
36
36
|
|
|
37
37
|
<div class="co-fields">
|
|
38
38
|
<div class="co-field">
|
|
39
|
-
<label class="co-field-label">{t.labelStockLength}</label>
|
|
40
|
-
<input type="number" data-co-stock-len data-co-cfg-inp value="2400" class="co-input" />
|
|
39
|
+
<label class="co-field-label" for="co-stock-length">{t.labelStockLength}</label>
|
|
40
|
+
<input type="number" id="co-stock-length" data-co-stock-len data-co-cfg-inp value="2400" class="co-input" />
|
|
41
41
|
</div>
|
|
42
42
|
<div class="co-field co-hidden" data-co-stock-wid-wrap>
|
|
43
|
-
<label class="co-field-label">{t.labelStockWidth}</label>
|
|
44
|
-
<input type="number" data-co-stock-wid data-co-cfg-inp value="1200" class="co-input" />
|
|
43
|
+
<label class="co-field-label" for="co-stock-width">{t.labelStockWidth}</label>
|
|
44
|
+
<input type="number" id="co-stock-width" data-co-stock-wid data-co-cfg-inp value="1200" class="co-input" />
|
|
45
45
|
</div>
|
|
46
46
|
<div class="co-field">
|
|
47
|
-
<label class="co-field-label">{t.labelKerf}</label>
|
|
48
|
-
<input type="number" data-co-kerf data-co-cfg-inp value="3" class="co-input" />
|
|
47
|
+
<label class="co-field-label" for="co-kerf">{t.labelKerf}</label>
|
|
48
|
+
<input type="number" id="co-kerf" data-co-kerf data-co-cfg-inp value="3" class="co-input" />
|
|
49
49
|
</div>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
@@ -100,15 +100,15 @@ const t = (ui ?? {}) as CutOptimizerUI;
|
|
|
100
100
|
<template data-co-row-tmpl>
|
|
101
101
|
<div class="co-row">
|
|
102
102
|
<div class="co-row-len-wrap">
|
|
103
|
-
<input type="number" data-co-inp-len placeholder={t.placeholderLength} class="co-row-input" />
|
|
103
|
+
<input type="number" aria-label={t.placeholderLength} data-co-inp-len placeholder={t.placeholderLength} class="co-row-input" />
|
|
104
104
|
<span class="co-row-unit">mm</span>
|
|
105
105
|
</div>
|
|
106
106
|
<div class="co-row-wid-wrap co-hidden" data-co-wid-wrap>
|
|
107
|
-
<input type="number" data-co-inp-wid placeholder={t.placeholderWidth} class="co-row-input co-row-input-blue" />
|
|
107
|
+
<input type="number" aria-label={t.placeholderWidth} data-co-inp-wid placeholder={t.placeholderWidth} class="co-row-input co-row-input-blue" />
|
|
108
108
|
<span class="co-row-unit">mm</span>
|
|
109
109
|
</div>
|
|
110
110
|
<div class="co-row-qty-wrap">
|
|
111
|
-
<input type="number" data-co-inp-qty placeholder={t.placeholderQty} value="1" class="co-row-input co-row-input-qty" />
|
|
111
|
+
<input type="number" aria-label={t.placeholderQty} data-co-inp-qty placeholder={t.placeholderQty} value="1" class="co-row-input co-row-input-qty" />
|
|
112
112
|
</div>
|
|
113
113
|
<button class="co-btn-rm" data-co-btn-rm title={t.btnRemoveTitle}>
|
|
114
114
|
<Icon name="mdi:close" />
|
|
@@ -74,7 +74,7 @@ const t = (ui ?? {}) as DrillCalculatorUI;
|
|
|
74
74
|
|
|
75
75
|
<div class="dc-field">
|
|
76
76
|
<div class="dc-diameter-header">
|
|
77
|
-
<label class="dc-field-label">{t.labelDiameter}</label>
|
|
77
|
+
<label class="dc-field-label" for="dc-diam-slider">{t.labelDiameter}</label>
|
|
78
78
|
<div class="dc-unit-toggle" id="dc-unit-toggle">
|
|
79
79
|
<button class="dc-unit-btn active" data-unit="mm">{t.unitMm}</button>
|
|
80
80
|
<button class="dc-unit-btn" data-unit="in">{t.unitInch}</button>
|
|
@@ -101,11 +101,11 @@ const t = (ui ?? {}) as DrillCalculatorUI;
|
|
|
101
101
|
<div class="dc-advanced-body" id="dc-adv-body">
|
|
102
102
|
<div class="dc-adv-grid">
|
|
103
103
|
<div>
|
|
104
|
-
<label class="dc-field-label">{t.labelLips}</label>
|
|
104
|
+
<label class="dc-field-label" for="dc-lips">{t.labelLips}</label>
|
|
105
105
|
<input type="number" id="dc-lips" class="dc-input" value="2" min="1" max="12" />
|
|
106
106
|
</div>
|
|
107
107
|
<div>
|
|
108
|
-
<label class="dc-field-label">{t.labelFz}</label>
|
|
108
|
+
<label class="dc-field-label" for="dc-fz">{t.labelFz}</label>
|
|
109
109
|
<input type="number" id="dc-fz" class="dc-input" placeholder="0.1" step="0.01" value="" />
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|
|
@@ -164,7 +164,7 @@ const t = (ui ?? {}) as DrillCalculatorUI;
|
|
|
164
164
|
<button class="dc-preset-btn" data-preset="cnc">{t.presetCnc}</button>
|
|
165
165
|
</div>
|
|
166
166
|
<div class="dc-manual-input">
|
|
167
|
-
<input type="number" id="dc-speed-in" class="dc-input" placeholder={t.addRpmManual} />
|
|
167
|
+
<input type="number" id="dc-speed-in" aria-label={t.addRpmManual} class="dc-input" placeholder={t.addRpmManual} />
|
|
168
168
|
<button id="dc-add-btn" class="dc-add-btn">+</button>
|
|
169
169
|
</div>
|
|
170
170
|
<div class="dc-chips-area" id="dc-chips-area"></div>
|
|
@@ -37,23 +37,23 @@ const t = (ui ?? {}) as EpoxyCalculatorUI;
|
|
|
37
37
|
|
|
38
38
|
<div class="ec-inputs-grid">
|
|
39
39
|
<div class="ec-input-group ec-rect-input">
|
|
40
|
-
<label class="ec-label">{t.labelLength}</label>
|
|
40
|
+
<label class="ec-label" for="ec-length">{t.labelLength}</label>
|
|
41
41
|
<input type="number" id="ec-length" value="30" class="ec-input" />
|
|
42
42
|
</div>
|
|
43
43
|
<div class="ec-input-group ec-rect-input">
|
|
44
|
-
<label class="ec-label">{t.labelWidth}</label>
|
|
44
|
+
<label class="ec-label" for="ec-width">{t.labelWidth}</label>
|
|
45
45
|
<input type="number" id="ec-width" value="20" class="ec-input" />
|
|
46
46
|
</div>
|
|
47
47
|
<div class="ec-input-group ec-cylinder-input ec-hidden">
|
|
48
|
-
<label class="ec-label">{t.labelDiameter}</label>
|
|
48
|
+
<label class="ec-label" for="ec-diameter">{t.labelDiameter}</label>
|
|
49
49
|
<input type="number" id="ec-diameter" value="15" class="ec-input" />
|
|
50
50
|
</div>
|
|
51
51
|
<div class="ec-input-group ec-sphere-input ec-hidden">
|
|
52
|
-
<label class="ec-label">{t.labelDiameter}</label>
|
|
52
|
+
<label class="ec-label" for="ec-sphere-diameter">{t.labelDiameter}</label>
|
|
53
53
|
<input type="number" id="ec-sphere-diameter" value="10" class="ec-input" />
|
|
54
54
|
</div>
|
|
55
55
|
<div class="ec-input-group ec-depth-input">
|
|
56
|
-
<label class="ec-label">{t.labelDepth}</label>
|
|
56
|
+
<label class="ec-label" for="ec-depth">{t.labelDepth}</label>
|
|
57
57
|
<input type="number" id="ec-depth" value="5" class="ec-input" />
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
@@ -66,7 +66,7 @@ const t = (ui ?? {}) as EpoxyCalculatorUI;
|
|
|
66
66
|
<span id="ec-ratio-display" class="ec-ratio-badge">2:1</span>
|
|
67
67
|
</div>
|
|
68
68
|
<div class="ec-slider-wrapper">
|
|
69
|
-
<input type="range" id="ec-ratio-slider" min="1" max="5" step="0.5" value="2" class="ec-slider" />
|
|
69
|
+
<input type="range" id="ec-ratio-slider" aria-label={t.labelRatio} min="1" max="5" step="0.5" value="2" class="ec-slider" />
|
|
70
70
|
<div class="ec-ratio-marks">
|
|
71
71
|
<span>1:1</span><span>2:1</span><span>3:1</span><span>4:1</span><span>5:1</span>
|
|
72
72
|
</div>
|
|
@@ -77,11 +77,11 @@ const t = (ui ?? {}) as EpoxyCalculatorUI;
|
|
|
77
77
|
</label>
|
|
78
78
|
<div id="ec-custom-inputs" class="ec-custom-grid ec-hidden">
|
|
79
79
|
<div>
|
|
80
|
-
<label class="ec-label">{t.labelResinA}</label>
|
|
80
|
+
<label class="ec-label" for="ec-ratio-a">{t.labelResinA}</label>
|
|
81
81
|
<input type="number" id="ec-ratio-a" value="100" class="ec-input ec-input-sm" />
|
|
82
82
|
</div>
|
|
83
83
|
<div>
|
|
84
|
-
<label class="ec-label">{t.labelHardenerB}</label>
|
|
84
|
+
<label class="ec-label" for="ec-ratio-b">{t.labelHardenerB}</label>
|
|
85
85
|
<input type="number" id="ec-ratio-b" value="50" class="ec-input ec-input-sm" />
|
|
86
86
|
</div>
|
|
87
87
|
</div>
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
.ec-root {
|
|
2
|
+
box-sizing: border-box;
|
|
2
3
|
width: 100%;
|
|
3
4
|
max-width: 72rem;
|
|
4
5
|
margin: 0 auto;
|
|
6
|
+
container-type: inline-size;
|
|
5
7
|
overflow-x: clip;
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
.ec-root *,
|
|
11
|
+
.ec-root *::before,
|
|
12
|
+
.ec-root *::after {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
.ec-grid {
|
|
9
17
|
display: grid;
|
|
10
18
|
grid-template-columns: 1fr;
|
|
11
19
|
gap: 2rem;
|
|
20
|
+
min-width: 0;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
@media (min-width: 1024px) {
|
|
@@ -18,6 +27,7 @@
|
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
.ec-panel-left {
|
|
30
|
+
min-width: 0;
|
|
21
31
|
background: rgba(255, 255, 255, 0.7);
|
|
22
32
|
backdrop-filter: blur(12px);
|
|
23
33
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
@@ -36,6 +46,7 @@
|
|
|
36
46
|
padding: 0.375rem;
|
|
37
47
|
border-radius: 1rem;
|
|
38
48
|
max-width: 100%;
|
|
49
|
+
min-width: 0;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
.ec-tab-btn {
|
|
@@ -682,3 +693,56 @@
|
|
|
682
693
|
min-height: 3.15rem;
|
|
683
694
|
}
|
|
684
695
|
}
|
|
696
|
+
|
|
697
|
+
@container (max-width: 720px) {
|
|
698
|
+
.ec-grid {
|
|
699
|
+
gap: 1rem;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.ec-panel-left,
|
|
703
|
+
.ec-result-inner,
|
|
704
|
+
.ec-viz-card {
|
|
705
|
+
min-width: 0;
|
|
706
|
+
padding: 0.875rem;
|
|
707
|
+
border-radius: 1rem;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.ec-tabs {
|
|
711
|
+
display: grid;
|
|
712
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
713
|
+
gap: 0.25rem;
|
|
714
|
+
overflow: hidden;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.ec-tab-btn {
|
|
718
|
+
min-width: 0;
|
|
719
|
+
padding: 0.55rem 0.35rem;
|
|
720
|
+
font-size: 0.75rem;
|
|
721
|
+
line-height: 1.15;
|
|
722
|
+
white-space: normal;
|
|
723
|
+
overflow-wrap: anywhere;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.ec-tab-icon {
|
|
727
|
+
flex-shrink: 0;
|
|
728
|
+
font-size: 1rem;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.ec-inputs-grid,
|
|
732
|
+
.ec-custom-grid,
|
|
733
|
+
.ec-parts-grid {
|
|
734
|
+
grid-template-columns: minmax(0, 1fr);
|
|
735
|
+
gap: 1rem;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.ec-waste-row {
|
|
739
|
+
gap: 1rem;
|
|
740
|
+
align-items: flex-start;
|
|
741
|
+
padding: 0.875rem;
|
|
742
|
+
overflow: hidden;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.ec-waste-info {
|
|
746
|
+
align-items: flex-start;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
@@ -28,16 +28,16 @@ const t = (ui ?? {}) as FurnitureFitUI;
|
|
|
28
28
|
</h2>
|
|
29
29
|
<div class="ff-inputs-grid">
|
|
30
30
|
<div class="ff-input-group">
|
|
31
|
-
<label class="ff-label">{t.labelWidth}</label>
|
|
32
|
-
<input type="number" data-ff-con-w data-ff-inp value="120" class="ff-input" />
|
|
31
|
+
<label class="ff-label" for="ff-con-w">{t.labelWidth}</label>
|
|
32
|
+
<input type="number" id="ff-con-w" data-ff-con-w data-ff-inp value="120" class="ff-input" />
|
|
33
33
|
</div>
|
|
34
34
|
<div class="ff-input-group">
|
|
35
|
-
<label class="ff-label">{t.labelHeight}</label>
|
|
36
|
-
<input type="number" data-ff-con-h data-ff-inp value="210" class="ff-input" />
|
|
35
|
+
<label class="ff-label" for="ff-con-h">{t.labelHeight}</label>
|
|
36
|
+
<input type="number" id="ff-con-h" data-ff-con-h data-ff-inp value="210" class="ff-input" />
|
|
37
37
|
</div>
|
|
38
38
|
<div class="ff-input-group">
|
|
39
|
-
<label class="ff-label">{t.labelDepth}</label>
|
|
40
|
-
<input type="number" data-ff-con-d data-ff-inp value="120" class="ff-input" />
|
|
39
|
+
<label class="ff-label" for="ff-con-d">{t.labelDepth}</label>
|
|
40
|
+
<input type="number" id="ff-con-d" data-ff-con-d data-ff-inp value="120" class="ff-input" />
|
|
41
41
|
</div>
|
|
42
42
|
</div>
|
|
43
43
|
<p class="ff-hint">{t.conHint}</p>
|
|
@@ -50,22 +50,22 @@ const t = (ui ?? {}) as FurnitureFitUI;
|
|
|
50
50
|
</h2>
|
|
51
51
|
<div class="ff-inputs-grid">
|
|
52
52
|
<div class="ff-input-group">
|
|
53
|
-
<label class="ff-label">{t.labelLength}</label>
|
|
54
|
-
<input type="number" data-ff-obj-w data-ff-inp value="220" class="ff-input" />
|
|
53
|
+
<label class="ff-label" for="ff-obj-w">{t.labelLength}</label>
|
|
54
|
+
<input type="number" id="ff-obj-w" data-ff-obj-w data-ff-inp value="220" class="ff-input" />
|
|
55
55
|
</div>
|
|
56
56
|
<div class="ff-input-group">
|
|
57
|
-
<label class="ff-label">{t.labelHeight}</label>
|
|
58
|
-
<input type="number" data-ff-obj-h data-ff-inp value="90" class="ff-input" />
|
|
57
|
+
<label class="ff-label" for="ff-obj-h">{t.labelHeight}</label>
|
|
58
|
+
<input type="number" id="ff-obj-h" data-ff-obj-h data-ff-inp value="90" class="ff-input" />
|
|
59
59
|
</div>
|
|
60
60
|
<div class="ff-input-group">
|
|
61
|
-
<label class="ff-label">{t.labelDepth}</label>
|
|
62
|
-
<input type="number" data-ff-obj-d data-ff-inp value="80" class="ff-input" />
|
|
61
|
+
<label class="ff-label" for="ff-obj-d">{t.labelDepth}</label>
|
|
62
|
+
<input type="number" id="ff-obj-d" data-ff-obj-d data-ff-inp value="80" class="ff-input" />
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
65
65
|
<div class="ff-margin-row">
|
|
66
66
|
<div class="ff-margin-left">
|
|
67
|
-
<label class="ff-label">{t.labelMargin}</label>
|
|
68
|
-
<input type="range" data-ff-margin data-ff-inp min="0" max="10" value="2" class="ff-slider" />
|
|
67
|
+
<label class="ff-label" for="ff-margin">{t.labelMargin}</label>
|
|
68
|
+
<input type="range" id="ff-margin" data-ff-margin data-ff-inp min="0" max="10" value="2" class="ff-slider" />
|
|
69
69
|
</div>
|
|
70
70
|
<span class="ff-margin-display" data-ff-margin-display>2cm</span>
|
|
71
71
|
</div>
|
|
@@ -94,7 +94,7 @@ const t = (ui ?? {}) as MortarCalculatorUI;
|
|
|
94
94
|
<g id="mc-sack-pile"></g>
|
|
95
95
|
</svg>
|
|
96
96
|
<div class="mc-slider-container">
|
|
97
|
-
<input type="range" data-mc-slider min="5" max="500" value="150" step="5" class="mc-slider" />
|
|
97
|
+
<input type="range" aria-label={t.labelQuantity} data-mc-slider min="5" max="500" value="150" step="5" class="mc-slider" />
|
|
98
98
|
<div class="mc-ruler-marks">
|
|
99
99
|
<span>5</span><span>100</span><span>200</span><span>300</span><span>400</span><span>500</span>
|
|
100
100
|
</div>
|
|
@@ -25,12 +25,12 @@ const t = (ui ?? {}) as PassepartoutCalculatorUI;
|
|
|
25
25
|
</div>
|
|
26
26
|
<div class="pp-row">
|
|
27
27
|
<div class="pp-field">
|
|
28
|
-
<label class="pp-label">{t.labelWidth}</label>
|
|
29
|
-
<input type="number" data-pp-input data-pp-frame-w class="pp-input" value="40" min="0" step="0.1" />
|
|
28
|
+
<label class="pp-label" for="pp-frame-w">{t.labelWidth}</label>
|
|
29
|
+
<input type="number" id="pp-frame-w" data-pp-input data-pp-frame-w class="pp-input" value="40" min="0" step="0.1" />
|
|
30
30
|
</div>
|
|
31
31
|
<div class="pp-field">
|
|
32
|
-
<label class="pp-label">{t.labelHeight}</label>
|
|
33
|
-
<input type="number" data-pp-input data-pp-frame-h class="pp-input" value="50" min="0" step="0.1" />
|
|
32
|
+
<label class="pp-label" for="pp-frame-h">{t.labelHeight}</label>
|
|
33
|
+
<input type="number" id="pp-frame-h" data-pp-input data-pp-frame-h class="pp-input" value="50" min="0" step="0.1" />
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
@@ -44,12 +44,12 @@ const t = (ui ?? {}) as PassepartoutCalculatorUI;
|
|
|
44
44
|
</div>
|
|
45
45
|
<div class="pp-row">
|
|
46
46
|
<div class="pp-field">
|
|
47
|
-
<label class="pp-label">{t.labelWidth}</label>
|
|
48
|
-
<input type="number" data-pp-input data-pp-art-w class="pp-input" value="28" min="0" step="0.1" />
|
|
47
|
+
<label class="pp-label" for="pp-art-w">{t.labelWidth}</label>
|
|
48
|
+
<input type="number" id="pp-art-w" data-pp-input data-pp-art-w class="pp-input" value="28" min="0" step="0.1" />
|
|
49
49
|
</div>
|
|
50
50
|
<div class="pp-field">
|
|
51
|
-
<label class="pp-label">{t.labelHeight}</label>
|
|
52
|
-
<input type="number" data-pp-input data-pp-art-h class="pp-input" value="38" min="0" step="0.1" />
|
|
51
|
+
<label class="pp-label" for="pp-art-h">{t.labelHeight}</label>
|
|
52
|
+
<input type="number" id="pp-art-h" data-pp-input data-pp-art-h class="pp-input" value="38" min="0" step="0.1" />
|
|
53
53
|
</div>
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
@@ -66,9 +66,9 @@ const t = (ui ?? {}) as PassepartoutCalculatorUI;
|
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
68
|
<div class="pp-offset-ctrl" data-pp-offset-ctrl>
|
|
69
|
-
<label class="pp-label">{t.labelOffset}</label>
|
|
69
|
+
<label class="pp-label" for="pp-offset">{t.labelOffset}</label>
|
|
70
70
|
<div class="pp-offset-row">
|
|
71
|
-
<input type="range" data-pp-input data-pp-offset-range class="pp-range" min="1" max="20" value="5" />
|
|
71
|
+
<input type="range" id="pp-offset" data-pp-input data-pp-offset-range class="pp-range" min="1" max="20" value="5" />
|
|
72
72
|
<span data-pp-offset-val class="pp-offset-val">5%</span>
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
.pp-root {
|
|
2
|
+
box-sizing: border-box;
|
|
2
3
|
width: 100%;
|
|
3
4
|
max-width: 1100px;
|
|
4
5
|
margin: 0 auto;
|
|
6
|
+
container-type: inline-size;
|
|
5
7
|
background: #fff;
|
|
6
8
|
border: 1px solid #e2e8f0;
|
|
7
9
|
border-radius: 1.5rem;
|
|
@@ -9,6 +11,12 @@
|
|
|
9
11
|
overflow: hidden;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
.pp-root *,
|
|
15
|
+
.pp-root *::before,
|
|
16
|
+
.pp-root *::after {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
.theme-dark .pp-root {
|
|
13
21
|
background: #1e1e2e;
|
|
14
22
|
border-color: #2d2d3f;
|
|
@@ -18,6 +26,7 @@
|
|
|
18
26
|
.pp-grid {
|
|
19
27
|
display: grid;
|
|
20
28
|
grid-template-columns: 1fr;
|
|
29
|
+
min-width: 0;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
@media (min-width: 1024px) {
|
|
@@ -27,6 +36,7 @@
|
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
.pp-inputs {
|
|
39
|
+
min-width: 0;
|
|
30
40
|
padding: 2rem;
|
|
31
41
|
background: #f8fafc;
|
|
32
42
|
display: flex;
|
|
@@ -624,3 +634,70 @@
|
|
|
624
634
|
width: min(100%, 70vw);
|
|
625
635
|
}
|
|
626
636
|
}
|
|
637
|
+
|
|
638
|
+
@container (max-width: 720px) {
|
|
639
|
+
.pp-root {
|
|
640
|
+
border-radius: 0.875rem;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.pp-inputs {
|
|
644
|
+
padding: 1rem;
|
|
645
|
+
gap: 1.25rem;
|
|
646
|
+
border-right: 0;
|
|
647
|
+
border-bottom: 1px solid #e2e8f0;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.theme-dark .pp-inputs {
|
|
651
|
+
border-bottom-color: #2d2d3f;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.pp-row {
|
|
655
|
+
grid-template-columns: minmax(0, 1fr);
|
|
656
|
+
gap: 0.75rem;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.pp-toggle-label {
|
|
660
|
+
align-items: flex-start;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.pp-preview {
|
|
664
|
+
min-height: 360px;
|
|
665
|
+
padding: 3.25rem 1rem;
|
|
666
|
+
overflow: hidden;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.pp-frame-wrapper {
|
|
670
|
+
width: min(100%, 76%);
|
|
671
|
+
max-height: 320px;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.pp-border-left {
|
|
675
|
+
left: 0.35rem;
|
|
676
|
+
align-items: flex-start;
|
|
677
|
+
padding-right: 0;
|
|
678
|
+
transform: translateY(-50%);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.pp-border-right {
|
|
682
|
+
right: 0.35rem;
|
|
683
|
+
align-items: flex-end;
|
|
684
|
+
padding-left: 0;
|
|
685
|
+
transform: translateY(-50%);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.pp-border-val {
|
|
689
|
+
font-size: 0.95rem;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.pp-border-tag,
|
|
693
|
+
.pp-border-unit {
|
|
694
|
+
font-size: 0.55rem;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.pp-border-tag {
|
|
698
|
+
letter-spacing: 0.04em;
|
|
699
|
+
max-width: 2.75rem;
|
|
700
|
+
overflow-wrap: anywhere;
|
|
701
|
+
text-align: inherit;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.sc-root {
|
|
25
|
+
box-sizing: border-box;
|
|
25
26
|
width: 100%;
|
|
26
27
|
max-width: 1100px;
|
|
27
28
|
margin: 1.5rem auto;
|
|
@@ -31,7 +32,15 @@
|
|
|
31
32
|
overflow-x: clip;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
.sc-root *,
|
|
36
|
+
.sc-root *::before,
|
|
37
|
+
.sc-root *::after {
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
.sc-card {
|
|
42
|
+
width: 100%;
|
|
43
|
+
max-width: 100%;
|
|
35
44
|
background: var(--sc-bg);
|
|
36
45
|
border-radius: 2rem;
|
|
37
46
|
overflow: hidden;
|
|
@@ -21,7 +21,7 @@ const t = (ui ?? {}) as TwoStrokeMixtureCalculatorUI;
|
|
|
21
21
|
|
|
22
22
|
<div class="tsmc-container">
|
|
23
23
|
<div class="tsmc-input-section">
|
|
24
|
-
<label id="tsmc-volume-label" class="tsmc-section-label">{t.labelVolume}</label>
|
|
24
|
+
<label id="tsmc-volume-label" class="tsmc-section-label" for="tsmc-slider">{t.labelVolume}</label>
|
|
25
25
|
|
|
26
26
|
<div class="tsmc-slider-container">
|
|
27
27
|
<input
|
|
@@ -99,25 +99,25 @@ const SECTIONS = [
|
|
|
99
99
|
|
|
100
100
|
<div class="vd-card vd-load-card">
|
|
101
101
|
<div class="vd-load-top">
|
|
102
|
-
<label class="vd-field-label">{t.fieldLoad}</label>
|
|
102
|
+
<label class="vd-field-label" for="vd-load-val">{t.fieldLoad}</label>
|
|
103
103
|
<div class="vd-mode-wrap">
|
|
104
104
|
<button class="vd-mode-btn vd-active" data-vd-mode-btn="watts">{t.modeW}</button>
|
|
105
105
|
<button class="vd-mode-btn" data-vd-mode-btn="amps">{t.modeA}</button>
|
|
106
106
|
</div>
|
|
107
107
|
</div>
|
|
108
108
|
<div class="vd-load-input-row">
|
|
109
|
-
<input type="number" data-vd-load-val value="60" class="vd-load-input" />
|
|
109
|
+
<input type="number" id="vd-load-val" data-vd-load-val value="60" class="vd-load-input" />
|
|
110
110
|
<span class="vd-load-unit" data-vd-load-unit>{t.unitWatts}</span>
|
|
111
111
|
</div>
|
|
112
112
|
</div>
|
|
113
113
|
|
|
114
114
|
<div class="vd-card">
|
|
115
115
|
<div class="vd-len-top">
|
|
116
|
-
<label class="vd-field-label">{t.fieldLength}</label>
|
|
116
|
+
<label class="vd-field-label" for="vd-len-slider">{t.fieldLength}</label>
|
|
117
117
|
<span class="vd-len-info"><span data-vd-len-display>10</span> {t.unitMeters}</span>
|
|
118
118
|
</div>
|
|
119
119
|
<div class="vd-slider-wrap">
|
|
120
|
-
<input type="range" data-vd-len-slider min="1" max="100" step="1" value="10" class="vd-slider-input" />
|
|
120
|
+
<input type="range" id="vd-len-slider" data-vd-len-slider min="1" max="100" step="1" value="10" class="vd-slider-input" />
|
|
121
121
|
<div class="vd-slider-track">
|
|
122
122
|
<div class="vd-slider-fill" data-vd-len-track style="width:9.09%"></div>
|
|
123
123
|
</div>
|
|
@@ -26,7 +26,7 @@ const t = (ui ?? {}) as WorkshopFractionConverterUI;
|
|
|
26
26
|
<label class="wfc-field-label">{t.labelFraction}</label>
|
|
27
27
|
<div class="wfc-fraction-input">
|
|
28
28
|
<div class="wfc-fraction-column">
|
|
29
|
-
<
|
|
29
|
+
<label class="wfc-fraction-label" for="wfc-whole">{t.labelWhole}</label>
|
|
30
30
|
<div class="wfc-stepper-group">
|
|
31
31
|
<button class="wfc-stepper-btn" data-stepper="whole-down">−</button>
|
|
32
32
|
<button class="wfc-stepper-btn" data-stepper="whole-up">+</button>
|
|
@@ -42,7 +42,7 @@ const t = (ui ?? {}) as WorkshopFractionConverterUI;
|
|
|
42
42
|
/>
|
|
43
43
|
</div>
|
|
44
44
|
<div class="wfc-fraction-column">
|
|
45
|
-
<
|
|
45
|
+
<label class="wfc-fraction-label" for="wfc-numerator">{t.labelNumerator}</label>
|
|
46
46
|
<div class="wfc-stepper-group">
|
|
47
47
|
<button class="wfc-stepper-btn" data-stepper="num-down">−</button>
|
|
48
48
|
<button class="wfc-stepper-btn" data-stepper="num-up">+</button>
|
|
@@ -58,7 +58,7 @@ const t = (ui ?? {}) as WorkshopFractionConverterUI;
|
|
|
58
58
|
/>
|
|
59
59
|
</div>
|
|
60
60
|
<div class="wfc-fraction-column">
|
|
61
|
-
<
|
|
61
|
+
<label class="wfc-fraction-label" for="wfc-denominator">{t.labelDenominator}</label>
|
|
62
62
|
<div class="wfc-stepper-group">
|
|
63
63
|
<button class="wfc-stepper-btn" data-stepper="denom-down">−</button>
|
|
64
64
|
<button class="wfc-stepper-btn" data-stepper="denom-up">+</button>
|
|
@@ -91,7 +91,7 @@ const t = (ui ?? {}) as WorkshopFractionConverterUI;
|
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
93
|
<div class="wfc-field wfc-hidden" id="wfc-reverse-input" data-reverse-input>
|
|
94
|
-
<label class="wfc-field-label">{t.reverseSearchLabel}</label>
|
|
94
|
+
<label class="wfc-field-label" for="wfc-reverse-mm">{t.reverseSearchLabel}</label>
|
|
95
95
|
<div style="display: flex; gap: 0.5rem;">
|
|
96
96
|
<input
|
|
97
97
|
type="number"
|