@jjlmoya/utils-audiovisual 1.20.0 → 1.21.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/layouts/PreviewLayout.astro +7 -2
- package/src/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tool/chromaticLens/component.astro +2 -2
- package/src/tool/collageMaker/component.astro +3 -3
- package/src/tool/depthOfFieldCalculator/component.astro +1 -1
- package/src/tool/exifCleaner/component.astro +1 -1
- package/src/tool/imageCompressor/component.astro +6 -4
- package/src/tool/printQualityCalculator/component.astro +3 -3
- package/src/tool/privacyBlur/component.astro +2 -2
- package/src/tool/subtitleSync/component.astro +2 -2
- package/src/tool/tvDistance/component.astro +1 -1
- package/src/tool/videoFrameExtractor/component.astro +3 -3
- package/src/tool/videoMerger/component.astro +3 -3
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-audiovisual",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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",
|
|
@@ -10,7 +10,12 @@ interface Props {
|
|
|
10
10
|
hasSidebar?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
title,
|
|
15
|
+
currentLocale = "es",
|
|
16
|
+
localeUrls = {},
|
|
17
|
+
hasSidebar = false,
|
|
18
|
+
} = Astro.props;
|
|
14
19
|
---
|
|
15
20
|
|
|
16
21
|
<!doctype html>
|
|
@@ -78,6 +83,7 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
|
|
|
78
83
|
transition:
|
|
79
84
|
background-color 0.3s ease,
|
|
80
85
|
color 0.3s ease;
|
|
86
|
+
font-family: Inter, sans-serif;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
main {
|
|
@@ -114,4 +120,3 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
|
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
</style>
|
|
117
|
-
|
|
@@ -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,7 +12,7 @@ const { ui } = Astro.props;
|
|
|
12
12
|
<div class="cl-card">
|
|
13
13
|
|
|
14
14
|
<div id="cl-empty" class="cl-drop">
|
|
15
|
-
<input type="file" id="cl-file" accept="image/*" class="cl-hidden" />
|
|
15
|
+
<input type="file" id="cl-file" accept="image/*" class="cl-hidden" aria-label={ui.changeImage} />
|
|
16
16
|
<div class="cl-drop-icon">
|
|
17
17
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
18
18
|
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
|
|
@@ -33,7 +33,7 @@ const { ui } = Astro.props;
|
|
|
33
33
|
<div class="cl-config-bar">
|
|
34
34
|
<div class="cl-config-item">
|
|
35
35
|
<span class="cl-config-label">{ui.colorCountLabel}</span>
|
|
36
|
-
<select id="cl-count" class="cl-count-select">
|
|
36
|
+
<select id="cl-count" class="cl-count-select" aria-label={ui.colorCountLabel}>
|
|
37
37
|
<option value="5">5</option>
|
|
38
38
|
<option value="8" selected>8</option>
|
|
39
39
|
<option value="12">12</option>
|
|
@@ -16,7 +16,7 @@ const dropSub = (ui.dropSub ?? '').replace('{link}', `<span class="cm-drop-link"
|
|
|
16
16
|
<div class="cm-left-col">
|
|
17
17
|
|
|
18
18
|
<div id="cm-drop-zone" class="cm-drop-zone">
|
|
19
|
-
<input type="file" id="cm-file-input" accept="image/*" multiple />
|
|
19
|
+
<input type="file" id="cm-file-input" accept="image/*" multiple aria-label={ui.dropTitle} />
|
|
20
20
|
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
21
21
|
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
22
22
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
@@ -64,14 +64,14 @@ const dropSub = (ui.dropSub ?? '').replace('{link}', `<span class="cm-drop-link"
|
|
|
64
64
|
<input type="range" id="cm-border-width" min="0" max="30" value="0" class="cm-slider" />
|
|
65
65
|
</div>
|
|
66
66
|
<div class="cm-setting">
|
|
67
|
-
<label class="cm-setting-label">{ui.borderColorLabel}</label>
|
|
67
|
+
<label class="cm-setting-label" for="cm-border-color">{ui.borderColorLabel}</label>
|
|
68
68
|
<div class="cm-color-row">
|
|
69
69
|
<input type="color" id="cm-border-color" value="#ffffff" class="cm-color-swatch" />
|
|
70
70
|
<span id="cm-bc-val" class="cm-color-code">#FFFFFF</span>
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
|
73
73
|
<div class="cm-setting">
|
|
74
|
-
<label class="cm-setting-label">{ui.bgColorLabel}</label>
|
|
74
|
+
<label class="cm-setting-label" for="cm-bg-color">{ui.bgColorLabel}</label>
|
|
75
75
|
<div class="cm-color-row">
|
|
76
76
|
<input type="color" id="cm-bg-color" value="#1a1a1a" class="cm-color-swatch" />
|
|
77
77
|
<span id="cm-bg-val" class="cm-color-code">#1A1A1A</span>
|
|
@@ -37,7 +37,7 @@ const moreSensors = SENSOR_PRESETS.filter(s => !QUICK_SENSOR_KEYS.includes(s.ke
|
|
|
37
37
|
</div>
|
|
38
38
|
<div class="dof-sensor-row">
|
|
39
39
|
<div class="dof-select-wrapper">
|
|
40
|
-
<select id="dof-sensor-more" class="dof-select">
|
|
40
|
+
<select id="dof-sensor-more" class="dof-select" aria-label={ui.sensorLabel}>
|
|
41
41
|
<option value="">{ui.moreLabel}</option>
|
|
42
42
|
{moreSensors.map(s => (
|
|
43
43
|
<option value={s.key} data-coc={s.coc}>{s.label}</option>
|
|
@@ -12,7 +12,7 @@ const { ui } = Astro.props;
|
|
|
12
12
|
<div class="ec-card">
|
|
13
13
|
|
|
14
14
|
<div id="ec-initial" class="ec-drop">
|
|
15
|
-
<input type="file" id="ec-file" accept="image/jpeg,image/png,image/webp" class="ec-hidden" />
|
|
15
|
+
<input type="file" id="ec-file" accept="image/jpeg,image/png,image/webp" class="ec-hidden" aria-label={ui.dropTitle} />
|
|
16
16
|
<div class="ec-drop-icon">
|
|
17
17
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
18
18
|
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zm-4.35 3.96h-3v3H9v-3H6v-2h3V8h2v3h3v2z"/>
|
|
@@ -89,12 +89,14 @@ const { ui } = Astro.props;
|
|
|
89
89
|
<div class="ic-inline-editor" style="display:none">
|
|
90
90
|
<div class="ic-editor-controls">
|
|
91
91
|
<div class="ic-editor-group">
|
|
92
|
-
<label>{ui.qualityLabel}: <span class="ic-local-q-val">80</span
|
|
93
|
-
|
|
92
|
+
<label>{ui.qualityLabel}: <span class="ic-local-q-val">80</span>%
|
|
93
|
+
<input type="range" class="ic-local-quality" min="10" max="100" value="80" />
|
|
94
|
+
</label>
|
|
94
95
|
</div>
|
|
95
96
|
<div class="ic-editor-group">
|
|
96
|
-
<label>{ui.maxWidthLabel}
|
|
97
|
-
|
|
97
|
+
<label>{ui.maxWidthLabel}:
|
|
98
|
+
<input type="number" class="ic-local-width" placeholder="Original" />
|
|
99
|
+
</label>
|
|
98
100
|
</div>
|
|
99
101
|
</div>
|
|
100
102
|
<button class="ic-editor-close">{ui.closeBtn}</button>
|
|
@@ -15,7 +15,7 @@ const { ui } = Astro.props;
|
|
|
15
15
|
id="drop-zone"
|
|
16
16
|
class="pq-drop-zone pq-group"
|
|
17
17
|
>
|
|
18
|
-
<input type="file" id="file-input" class="pq-hidden" accept="image/webp, image/jpeg, image/png, image/tiff" />
|
|
18
|
+
<input type="file" id="file-input" class="pq-hidden" accept="image/webp, image/jpeg, image/png, image/tiff" aria-label={ui.dropTitle} />
|
|
19
19
|
|
|
20
20
|
<div class="pq-drop-inner">
|
|
21
21
|
<div class="pq-upload-icon-wrap pq-group-icon">
|
|
@@ -64,7 +64,7 @@ const { ui } = Astro.props;
|
|
|
64
64
|
|
|
65
65
|
<div class="pq-main-grid">
|
|
66
66
|
<div class="pq-config-panel">
|
|
67
|
-
<label class="pq-config-label">{ui.dpiDensityLabel}</label>
|
|
67
|
+
<label class="pq-config-label" for="dpi-slider">{ui.dpiDensityLabel}</label>
|
|
68
68
|
|
|
69
69
|
<input
|
|
70
70
|
type="range"
|
|
@@ -83,7 +83,7 @@ const { ui } = Astro.props;
|
|
|
83
83
|
value="300"
|
|
84
84
|
class="pq-dpi-number"
|
|
85
85
|
/>
|
|
86
|
-
<
|
|
86
|
+
<label class="pq-dpi-label" for="dpi-input">{ui.dpiPointsLabel}</label>
|
|
87
87
|
</div>
|
|
88
88
|
|
|
89
89
|
<div class="pq-presets">
|
|
@@ -39,7 +39,7 @@ const { ui } = Astro.props;
|
|
|
39
39
|
<svg viewBox="0 0 24 24" class="pb-icon" aria-hidden="true" fill="currentColor">
|
|
40
40
|
<path d="M3 6h18v2H3zm3-4v4M12 4v4M21 6h-3M9 11v4M15 11v4M3 12h6m6 0h6M3 18h18v2H3zm3-4v4m6-4v4m6-4v4"/>
|
|
41
41
|
</svg>
|
|
42
|
-
<input type="range" id="pb-intensity" min="1" max="50" value="10" class="pb-slider" />
|
|
42
|
+
<input type="range" id="pb-intensity" min="1" max="50" value="10" class="pb-slider" aria-label="Intensity" />
|
|
43
43
|
</div>
|
|
44
44
|
<button id="pb-auto-face" class="pb-auto-btn">
|
|
45
45
|
<svg viewBox="0 0 24 24" class="pb-icon" aria-hidden="true" fill="currentColor">
|
|
@@ -65,7 +65,7 @@ const { ui } = Astro.props;
|
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
67
|
<div id="pb-workspace" class="pb-workspace">
|
|
68
|
-
<input type="file" id="pb-file-input" accept="image/*" class="pb-hidden" />
|
|
68
|
+
<input type="file" id="pb-file-input" accept="image/*" class="pb-hidden" aria-label={ui.dropTitle} />
|
|
69
69
|
|
|
70
70
|
<div id="pb-empty" class="pb-empty">
|
|
71
71
|
<div class="pb-upload-icon">
|
|
@@ -12,7 +12,7 @@ const { ui } = Astro.props;
|
|
|
12
12
|
<div class="ss-card">
|
|
13
13
|
|
|
14
14
|
<div id="ss-drop" class="ss-drop">
|
|
15
|
-
<input type="file" id="ss-file" accept=".srt,.vtt" class="ss-hidden" />
|
|
15
|
+
<input type="file" id="ss-file" accept=".srt,.vtt" class="ss-hidden" aria-label={ui.dropTitle} />
|
|
16
16
|
<div class="ss-drop-icon">
|
|
17
17
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
18
18
|
<path d="M17 6h-1V3H8v3H7c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-5 2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm5 12H7V8h10v12zm-1-8h-2v4h2v-4zm-4 0H8v4h2v-4z"/>
|
|
@@ -24,7 +24,7 @@ const { ui } = Astro.props;
|
|
|
24
24
|
|
|
25
25
|
<div id="ss-controls" class="ss-controls ss-hidden">
|
|
26
26
|
<div class="ss-control-group">
|
|
27
|
-
<
|
|
27
|
+
<label class="ss-control-label" for="ss-offset">{ui.offsetLabel}</label>
|
|
28
28
|
<div class="ss-offset-wrap">
|
|
29
29
|
<button id="ss-dec" class="ss-offset-btn">
|
|
30
30
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
@@ -25,7 +25,7 @@ const { ui } = Astro.props;
|
|
|
25
25
|
|
|
26
26
|
<div class="tvd-field">
|
|
27
27
|
<div class="tvd-field-row">
|
|
28
|
-
<
|
|
28
|
+
<label class="tvd-label" for="tvd-diagonal">{ui.diagonalLabel}</label>
|
|
29
29
|
<span id="tvd-diagonal-display" class="tvd-diagonal-val">55"</span>
|
|
30
30
|
</div>
|
|
31
31
|
<input type="range" id="tvd-diagonal" min="32" max="100" value="55" class="tvd-slider" />
|
|
@@ -12,7 +12,7 @@ const { ui } = Astro.props;
|
|
|
12
12
|
<div class="vfe-premium-card">
|
|
13
13
|
|
|
14
14
|
<div id="vfe-upload-box" class="vfe-uploader-box">
|
|
15
|
-
<input type="file" id="vfe-file-input" accept="video/*" class="vfe-hidden" />
|
|
15
|
+
<input type="file" id="vfe-file-input" accept="video/*" class="vfe-hidden" aria-label={ui.uploadTitle} />
|
|
16
16
|
<div class="vfe-uploader-icon">
|
|
17
17
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
18
18
|
<path d="M17 10.5V7a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11zM14 13h-3v3H9v-3H6v-2h3V8h2v3h3z"/>
|
|
@@ -35,7 +35,7 @@ const { ui } = Astro.props;
|
|
|
35
35
|
<span id="vfe-current-time">00:00.000</span>
|
|
36
36
|
<span id="vfe-duration">00:00.000</span>
|
|
37
37
|
</div>
|
|
38
|
-
<input type="range" id="vfe-scrubber" class="vfe-scrubber" min="0" step="0.001" value="0" />
|
|
38
|
+
<input type="range" id="vfe-scrubber" class="vfe-scrubber" min="0" step="0.001" value="0" aria-label="Timeline" />
|
|
39
39
|
|
|
40
40
|
<div class="vfe-actions-row">
|
|
41
41
|
<button id="vfe-prev" class="vfe-btn-main vfe-btn-control" title={ui.prevFrame}>
|
|
@@ -70,7 +70,7 @@ const { ui } = Astro.props;
|
|
|
70
70
|
</div>
|
|
71
71
|
<div class="vfe-batch-controls">
|
|
72
72
|
<div class="vfe-batch-input-group">
|
|
73
|
-
<label>{ui.batchEvery}</label>
|
|
73
|
+
<label for="vfe-batch-interval">{ui.batchEvery}</label>
|
|
74
74
|
<input type="number" id="vfe-batch-interval" value="1" min="0.1" step="0.1" />
|
|
75
75
|
<label>s</label>
|
|
76
76
|
</div>
|
|
@@ -13,7 +13,7 @@ const { ui } = Astro.props;
|
|
|
13
13
|
<div class="vm-premium-card">
|
|
14
14
|
|
|
15
15
|
<div id="vm-upload-box" class="vm-uploader-box">
|
|
16
|
-
<input type="file" id="vm-file-input" accept="video/*" multiple class="vm-hidden" />
|
|
16
|
+
<input type="file" id="vm-file-input" accept="video/*" multiple class="vm-hidden" aria-label={ui.uploadTitle} />
|
|
17
17
|
<div class="vm-uploader-icon">
|
|
18
18
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
19
19
|
<path d="M17 10.5V7a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11zM14 13h-3v3H9v-3H6v-2h3V8h2v3h3z"/>
|
|
@@ -47,7 +47,7 @@ const { ui } = Astro.props;
|
|
|
47
47
|
|
|
48
48
|
<div class="vm-options-grid">
|
|
49
49
|
<div class="vm-select-group">
|
|
50
|
-
<label>{ui.optionResolution}</label>
|
|
50
|
+
<label for="vm-res-select">{ui.optionResolution}</label>
|
|
51
51
|
<select id="vm-res-select">
|
|
52
52
|
<option value="1920x1080">1920x1080 (Full HD 16:9)</option>
|
|
53
53
|
<option value="1280x720">1280x720 (HD 16:9)</option>
|
|
@@ -58,7 +58,7 @@ const { ui } = Astro.props;
|
|
|
58
58
|
</div>
|
|
59
59
|
|
|
60
60
|
<div class="vm-select-group">
|
|
61
|
-
<label>{ui.optionFps}</label>
|
|
61
|
+
<label for="vm-fps-select">{ui.optionFps}</label>
|
|
62
62
|
<select id="vm-fps-select">
|
|
63
63
|
<option value="30">30 FPS</option>
|
|
64
64
|
<option value="24">24 FPS</option>
|