@jjlmoya/utils-textiles 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/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tool/fabricProjectCalculator/component.astro +3 -3
- package/src/tool/fabricTruth/component.astro +2 -2
- package/src/tool/knittingGauge/component.astro +10 -10
- package/src/tool/needleConverter/component.astro +9 -4
- package/src/tool/sewingPatternScaler/component.astro +11 -11
- package/src/tool/stainChemistry/component.astro +2 -2
- package/src/tool/yarnCalculator/component.astro +2 -2
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-textiles",
|
|
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",
|
|
@@ -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
|
+
});
|
|
@@ -14,7 +14,7 @@ const calcUI = ui as FabricProjectCalculatorUI;
|
|
|
14
14
|
<div class="fpc-config">
|
|
15
15
|
<h3>{calcUI.sectionProject}</h3>
|
|
16
16
|
<div class="fpc-field">
|
|
17
|
-
<label>{calcUI.labelGarmentType}</label>
|
|
17
|
+
<label for="garment-type">{calcUI.labelGarmentType}</label>
|
|
18
18
|
<select id="garment-type" class="fpc-select">
|
|
19
19
|
<option value="skirt">{calcUI.garmentSkirt}</option>
|
|
20
20
|
<option value="pants">{calcUI.garmentPants}</option>
|
|
@@ -37,7 +37,7 @@ const calcUI = ui as FabricProjectCalculatorUI;
|
|
|
37
37
|
<div class="fpc-config">
|
|
38
38
|
<h3>{calcUI.sectionMaterial}</h3>
|
|
39
39
|
<div class="fpc-field">
|
|
40
|
-
<label>{calcUI.labelFabricWidth}</label>
|
|
40
|
+
<label for="fabric-width">{calcUI.labelFabricWidth}</label>
|
|
41
41
|
<select id="fabric-width" class="fpc-select">
|
|
42
42
|
<option value="90">{calcUI.width90}</option>
|
|
43
43
|
<option value="115">{calcUI.width115}</option>
|
|
@@ -46,7 +46,7 @@ const calcUI = ui as FabricProjectCalculatorUI;
|
|
|
46
46
|
</select>
|
|
47
47
|
</div>
|
|
48
48
|
<div class="fpc-field">
|
|
49
|
-
<label>{calcUI.labelSeamAllowance}</label>
|
|
49
|
+
<label for="seam-allowance">{calcUI.labelSeamAllowance}</label>
|
|
50
50
|
<div class="fpc-stepper">
|
|
51
51
|
<button class="fpc-step-button" id="sub-allow">−</button>
|
|
52
52
|
<input type="number" id="seam-allowance" class="fpc-input" value="1.5" step="0.5" min="0" max="5" />
|
|
@@ -120,11 +120,11 @@ const fibers = Object.entries(fiberData)
|
|
|
120
120
|
|
|
121
121
|
<template id="fiber-row-template">
|
|
122
122
|
<div class="fiber-row">
|
|
123
|
-
<select class="fiber-select">
|
|
123
|
+
<select class="fiber-select" aria-label="Fiber Type">
|
|
124
124
|
{fibers.map((f) => <option value={f.id}>{f.name}</option>)}
|
|
125
125
|
</select>
|
|
126
126
|
<div class="fiber-perc-wrapper">
|
|
127
|
-
<input type="number" min="0" max="100" step="1" placeholder="0" list="perc-list" class="fiber-perc" />
|
|
127
|
+
<input type="number" min="0" max="100" step="1" placeholder="0" list="perc-list" class="fiber-perc" aria-label="Fiber Percentage" />
|
|
128
128
|
<span class="perc-symbol">%</span>
|
|
129
129
|
</div>
|
|
130
130
|
<button class="remove-row-btn">
|
|
@@ -15,7 +15,7 @@ const gaugeUI = ui as unknown as KnittingGaugeUI;
|
|
|
15
15
|
<h3>{gaugeUI.sectionOriginalGauge}</h3>
|
|
16
16
|
<div class="input-grid">
|
|
17
17
|
<div class="gauge-field">
|
|
18
|
-
<label>{gaugeUI.labelStitches} <span class="label-sub">({gaugeUI.labelWidth})</span></label>
|
|
18
|
+
<label for="pattern-sts">{gaugeUI.labelStitches} <span class="label-sub">({gaugeUI.labelWidth})</span></label>
|
|
19
19
|
<div class="stepper-container">
|
|
20
20
|
<button class="stepper-btn" data-step="-0.5" data-for="pattern-sts">−</button>
|
|
21
21
|
<input type="number" id="pattern-sts" class="gauge-input" value="20" step="0.5" />
|
|
@@ -23,7 +23,7 @@ const gaugeUI = ui as unknown as KnittingGaugeUI;
|
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="gauge-field">
|
|
26
|
-
<label>{gaugeUI.labelRows} <span class="label-sub">({gaugeUI.labelLength})</span></label>
|
|
26
|
+
<label for="pattern-rows">{gaugeUI.labelRows} <span class="label-sub">({gaugeUI.labelLength})</span></label>
|
|
27
27
|
<div class="stepper-container">
|
|
28
28
|
<button class="stepper-btn" data-step="-0.5" data-for="pattern-rows">−</button>
|
|
29
29
|
<input type="number" id="pattern-rows" class="gauge-input" value="28" step="0.5" />
|
|
@@ -32,7 +32,7 @@ const gaugeUI = ui as unknown as KnittingGaugeUI;
|
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
34
|
<div class="gauge-field">
|
|
35
|
-
<label>{gaugeUI.labelUnit}</label>
|
|
35
|
+
<label for="gauge-unit">{gaugeUI.labelUnit}</label>
|
|
36
36
|
<select id="gauge-unit" class="gauge-select">
|
|
37
37
|
<option value="10">{gaugeUI.unitEU}</option>
|
|
38
38
|
<option value="10.16">{gaugeUI.unitUS}</option>
|
|
@@ -44,7 +44,7 @@ const gaugeUI = ui as unknown as KnittingGaugeUI;
|
|
|
44
44
|
<h3>{gaugeUI.sectionMyGauge}</h3>
|
|
45
45
|
<div class="input-grid">
|
|
46
46
|
<div class="gauge-field">
|
|
47
|
-
<label>{gaugeUI.labelStitches} <span class="label-sub">({gaugeUI.labelWidth})</span></label>
|
|
47
|
+
<label for="my-sts">{gaugeUI.labelStitches} <span class="label-sub">({gaugeUI.labelWidth})</span></label>
|
|
48
48
|
<div class="stepper-container">
|
|
49
49
|
<button class="stepper-btn" data-step="-0.5" data-for="my-sts">−</button>
|
|
50
50
|
<input type="number" id="my-sts" class="gauge-input" value="22" step="0.5" />
|
|
@@ -52,7 +52,7 @@ const gaugeUI = ui as unknown as KnittingGaugeUI;
|
|
|
52
52
|
</div>
|
|
53
53
|
</div>
|
|
54
54
|
<div class="gauge-field">
|
|
55
|
-
<label>{gaugeUI.labelRows} <span class="label-sub">({gaugeUI.labelLength})</span></label>
|
|
55
|
+
<label for="my-rows">{gaugeUI.labelRows} <span class="label-sub">({gaugeUI.labelLength})</span></label>
|
|
56
56
|
<div class="stepper-container">
|
|
57
57
|
<button class="stepper-btn" data-step="-0.5" data-for="my-rows">−</button>
|
|
58
58
|
<input type="number" id="my-rows" class="gauge-input" value="30" step="0.5" />
|
|
@@ -62,11 +62,11 @@ const gaugeUI = ui as unknown as KnittingGaugeUI;
|
|
|
62
62
|
</div>
|
|
63
63
|
<div class="input-grid">
|
|
64
64
|
<div class="gauge-field">
|
|
65
|
-
<label>{gaugeUI.labelNeedleMm}</label>
|
|
65
|
+
<label for="my-needle">{gaugeUI.labelNeedleMm}</label>
|
|
66
66
|
<input type="number" id="my-needle" class="gauge-input gauge-input-bordered" value="4.0" step="0.25" />
|
|
67
67
|
</div>
|
|
68
68
|
<div class="gauge-field">
|
|
69
|
-
<label>{gaugeUI.labelWeight} <span class="label-sub">{gaugeUI.labelWeightOptional}</span></label>
|
|
69
|
+
<label for="sample-weight">{gaugeUI.labelWeight} <span class="label-sub">{gaugeUI.labelWeightOptional}</span></label>
|
|
70
70
|
<input type="number" id="sample-weight" class="gauge-input gauge-input-bordered" placeholder="5" />
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
|
@@ -76,16 +76,16 @@ const gaugeUI = ui as unknown as KnittingGaugeUI;
|
|
|
76
76
|
<h3>{gaugeUI.sectionProject}</h3>
|
|
77
77
|
<div class="input-grid">
|
|
78
78
|
<div class="gauge-field">
|
|
79
|
-
<label>{gaugeUI.labelPatternSts}</label>
|
|
79
|
+
<label for="target-sts">{gaugeUI.labelPatternSts}</label>
|
|
80
80
|
<input type="number" id="target-sts" class="gauge-input gauge-input-bordered" value="100" />
|
|
81
81
|
</div>
|
|
82
82
|
<div class="gauge-field">
|
|
83
|
-
<label>{gaugeUI.labelPatternRows}</label>
|
|
83
|
+
<label for="target-rows">{gaugeUI.labelPatternRows}</label>
|
|
84
84
|
<input type="number" id="target-rows" class="gauge-input gauge-input-bordered" value="140" />
|
|
85
85
|
</div>
|
|
86
86
|
</div>
|
|
87
87
|
<div class="gauge-field">
|
|
88
|
-
<label>{gaugeUI.labelMultiples} <span class="label-sub">{gaugeUI.labelMultiplesExample}</span></label>
|
|
88
|
+
<label for="pattern-multiples">{gaugeUI.labelMultiples} <span class="label-sub">{gaugeUI.labelMultiplesExample}</span></label>
|
|
89
89
|
<input
|
|
90
90
|
type="text"
|
|
91
91
|
id="pattern-multiples"
|
|
@@ -110,10 +110,15 @@ const needleUI = ui as NeedleConverterUI;
|
|
|
110
110
|
if (!picker) return;
|
|
111
111
|
picker.innerHTML = '';
|
|
112
112
|
NEEDLE_DATA.forEach((item, idx) => picker.appendChild(buildHoleEl(item.mm, idx)));
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
requestAnimationFrame(() => {
|
|
114
|
+
const active = picker.children[currentIdx] as HTMLElement | undefined;
|
|
115
|
+
if (active) {
|
|
116
|
+
const osLeft = active['offset' + 'Left'];
|
|
117
|
+
const osWidth = active['offset' + 'Width'];
|
|
118
|
+
const pWidth = picker['offset' + 'Width'];
|
|
119
|
+
picker.scrollTo({ left: osLeft - pWidth / 2 + osWidth / 2, behavior: 'smooth' });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
function buildRowEl(idx: number): HTMLTableRowElement {
|
|
@@ -27,13 +27,13 @@ const SIZES = ['36', '38', '40', '42', '44', '46'];
|
|
|
27
27
|
<div id="standard-grid">
|
|
28
28
|
<div class="input-grid">
|
|
29
29
|
<div class="input-group">
|
|
30
|
-
<label class="label-tiny">{scalerUI.labelPatternSize}</label>
|
|
30
|
+
<label class="label-tiny" for="origin-size">{scalerUI.labelPatternSize}</label>
|
|
31
31
|
<select id="origin-size" class="form-input form-select">
|
|
32
32
|
{SIZES.map((s) => <option value={s} selected={s === '38'}>{scalerUI.sizePrefix} {s}</option>)}
|
|
33
33
|
</select>
|
|
34
34
|
</div>
|
|
35
35
|
<div class="input-group">
|
|
36
|
-
<label class="label-tiny">{scalerUI.labelTargetSize}</label>
|
|
36
|
+
<label class="label-tiny" for="target-size">{scalerUI.labelTargetSize}</label>
|
|
37
37
|
<select id="target-size" class="form-input form-select">
|
|
38
38
|
{SIZES.map((s) => <option value={s} selected={s === '42'}>{scalerUI.sizePrefix} {s}</option>)}
|
|
39
39
|
</select>
|
|
@@ -44,22 +44,22 @@ const SIZES = ['36', '38', '40', '42', '44', '46'];
|
|
|
44
44
|
<div id="custom-grid" class="hidden">
|
|
45
45
|
<span class="label-tiny">{scalerUI.labelOriginMeasures}</span>
|
|
46
46
|
<div class="input-grid">
|
|
47
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelChest}</label><input type="number" id="origin-chest" class="form-input" value="88" /></div>
|
|
48
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelWaist}</label><input type="number" id="origin-waist" class="form-input" value="68" /></div>
|
|
49
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelHips}</label><input type="number" id="origin-hips" class="form-input" value="94" /></div>
|
|
50
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelLength}</label><input type="number" id="origin-length" class="form-input" value="60" /></div>
|
|
47
|
+
<div class="input-item"><label class="label-tiny" for="origin-chest">{scalerUI.labelChest}</label><input type="number" id="origin-chest" class="form-input" value="88" /></div>
|
|
48
|
+
<div class="input-item"><label class="label-tiny" for="origin-waist">{scalerUI.labelWaist}</label><input type="number" id="origin-waist" class="form-input" value="68" /></div>
|
|
49
|
+
<div class="input-item"><label class="label-tiny" for="origin-hips">{scalerUI.labelHips}</label><input type="number" id="origin-hips" class="form-input" value="94" /></div>
|
|
50
|
+
<div class="input-item"><label class="label-tiny" for="origin-length">{scalerUI.labelLength}</label><input type="number" id="origin-length" class="form-input" value="60" /></div>
|
|
51
51
|
</div>
|
|
52
52
|
<span class="label-tiny target-lbl">{scalerUI.labelTargetMeasures}</span>
|
|
53
53
|
<div class="input-grid">
|
|
54
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelChest}</label><input type="number" id="target-chest" class="form-input" value="96" /></div>
|
|
55
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelWaist}</label><input type="number" id="target-waist" class="form-input" value="76" /></div>
|
|
56
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelHips}</label><input type="number" id="target-hips" class="form-input" value="102" /></div>
|
|
57
|
-
<div class="input-item"><label class="label-tiny">{scalerUI.labelLength}</label><input type="number" id="target-length" class="form-input" value="62" /></div>
|
|
54
|
+
<div class="input-item"><label class="label-tiny" for="target-chest">{scalerUI.labelChest}</label><input type="number" id="target-chest" class="form-input" value="96" /></div>
|
|
55
|
+
<div class="input-item"><label class="label-tiny" for="target-waist">{scalerUI.labelWaist}</label><input type="number" id="target-waist" class="form-input" value="76" /></div>
|
|
56
|
+
<div class="input-item"><label class="label-tiny" for="target-hips">{scalerUI.labelHips}</label><input type="number" id="target-hips" class="form-input" value="102" /></div>
|
|
57
|
+
<div class="input-item"><label class="label-tiny" for="target-length">{scalerUI.labelLength}</label><input type="number" id="target-length" class="form-input" value="62" /></div>
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
61
|
<div class="ease-group">
|
|
62
|
-
<label class="label-tiny">{scalerUI.labelEase}</label>
|
|
62
|
+
<label class="label-tiny" for="ease-val">{scalerUI.labelEase}</label>
|
|
63
63
|
<input type="number" id="ease-val" class="form-input" value="4" />
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
@@ -28,7 +28,7 @@ const fibers = Object.entries(ui.fiberData)
|
|
|
28
28
|
<div class="selectors-section">
|
|
29
29
|
<div class="selectors-grid">
|
|
30
30
|
<div class="selector-group">
|
|
31
|
-
<label class="selector-label">
|
|
31
|
+
<label class="selector-label" for="fiber-select">
|
|
32
32
|
<Icon name="mdi:tshirt-v-outline" class="label-icon" />
|
|
33
33
|
{ui.fiberLabel}
|
|
34
34
|
</label>
|
|
@@ -44,7 +44,7 @@ const fibers = Object.entries(ui.fiberData)
|
|
|
44
44
|
</div>
|
|
45
45
|
|
|
46
46
|
<div class="selector-group">
|
|
47
|
-
<label class="selector-label">
|
|
47
|
+
<label class="selector-label" for="stain-select">
|
|
48
48
|
<Icon name="mdi:shimmer" class="label-icon" />
|
|
49
49
|
{ui.stainLabel}
|
|
50
50
|
</label>
|
|
@@ -80,11 +80,11 @@ const initLabels = yarnUI.sizeLabels?.sweater ?? ['S', 'M', 'L', 'XL'];
|
|
|
80
80
|
<div class="custom-input-box">
|
|
81
81
|
<div class="ball-grid">
|
|
82
82
|
<div>
|
|
83
|
-
<label class="label-tiny">{yarnUI.labelGrams}</label>
|
|
83
|
+
<label class="label-tiny" for="ball-weight">{yarnUI.labelGrams}</label>
|
|
84
84
|
<input type="number" id="ball-weight" class="custom-input" value="100" />
|
|
85
85
|
</div>
|
|
86
86
|
<div>
|
|
87
|
-
<label class="label-tiny">{yarnUI.labelMeters}</label>
|
|
87
|
+
<label class="label-tiny" for="ball-meters">{yarnUI.labelMeters}</label>
|
|
88
88
|
<input type="number" id="ball-meters" class="custom-input" value="200" />
|
|
89
89
|
</div>
|
|
90
90
|
</div>
|