@jjlmoya/utils-games-development 1.54.0 → 1.56.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 +1 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +2 -0
- package/src/tests/contrast_legibility.test.ts +291 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
- package/src/tool/localizationSanitizer/localization-sanitizer.css +17 -15
- package/src/tool/pixelArtPaletteSwapper/app-client.ts +229 -0
- package/src/tool/pixelArtPaletteSwapper/bibliography.astro +6 -0
- package/src/tool/pixelArtPaletteSwapper/bibliography.ts +16 -0
- package/src/tool/pixelArtPaletteSwapper/component.astro +153 -0
- package/src/tool/pixelArtPaletteSwapper/entry.ts +28 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/de.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/en.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/es.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/fr.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/id.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/it.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +225 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +225 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/nl.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/pl.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/pt.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ru.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/sv.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/tr.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +225 -0
- package/src/tool/pixelArtPaletteSwapper/index.ts +12 -0
- package/src/tool/pixelArtPaletteSwapper/logic.test.ts +43 -0
- package/src/tool/pixelArtPaletteSwapper/logic.ts +171 -0
- package/src/tool/pixelArtPaletteSwapper/pixel-art-palette-swapper.css +728 -0
- package/src/tool/pixelArtPaletteSwapper/seo.astro +15 -0
- package/src/tool/pixelArtPaletteSwapper/ui.ts +35 -0
- package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
- package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
- package/src/tool/retroSfxGenerator/client.ts +263 -0
- package/src/tool/retroSfxGenerator/component.astro +146 -0
- package/src/tool/retroSfxGenerator/entry.ts +27 -0
- package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
- package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
- package/src/tool/retroSfxGenerator/index.ts +11 -0
- package/src/tool/retroSfxGenerator/logic.test.ts +61 -0
- package/src/tool/retroSfxGenerator/logic.ts +214 -0
- package/src/tool/retroSfxGenerator/retro-sfx-generator.css +435 -0
- package/src/tool/retroSfxGenerator/seo.astro +15 -0
- package/src/tool/retroSfxGenerator/ui.ts +41 -0
- package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
- package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
- package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +8 -8
- package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
- package/src/tools.ts +4 -0
package/package.json
CHANGED
package/src/entries.ts
CHANGED
|
@@ -12,6 +12,10 @@ export { localizationSanitizer } from './tool/localizationSanitizer/entry';
|
|
|
12
12
|
export type { LocalizationSanitizerUI, LocalizationSanitizerLocaleContent } from './tool/localizationSanitizer/entry';
|
|
13
13
|
export { steamBbcodeTranslator } from './tool/steamBbcodeTranslator/entry';
|
|
14
14
|
export type { SteamBbcodeTranslatorUI, SteamBbcodeTranslatorLocaleContent } from './tool/steamBbcodeTranslator/entry';
|
|
15
|
+
export { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
|
|
16
|
+
export type { RetroSfxGeneratorUI, RetroSfxGeneratorLocaleContent } from './tool/retroSfxGenerator/entry';
|
|
17
|
+
export { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
|
|
18
|
+
export type { PixelArtPaletteSwapperUI, PixelArtPaletteSwapperLocaleContent } from './tool/pixelArtPaletteSwapper/entry';
|
|
15
19
|
export { gamesCategory } from './category';
|
|
16
20
|
import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
|
|
17
21
|
import { itchioGameTester } from './tool/itchioGameTester/entry';
|
|
@@ -19,5 +23,7 @@ import { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
|
|
|
19
23
|
import { audioLoopPointFinder } from './tool/audioLoopPointFinder/entry';
|
|
20
24
|
import { saveFileEditor } from './tool/saveFileEditor/entry';
|
|
21
25
|
import { localizationSanitizer } from './tool/localizationSanitizer/entry';
|
|
26
|
+
import { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
|
|
27
|
+
import { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
|
|
22
28
|
|
|
23
|
-
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer];
|
|
29
|
+
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, retroSfxGenerator, pixelArtPaletteSwapper];
|
package/src/index.ts
CHANGED
|
@@ -18,5 +18,7 @@ export type {
|
|
|
18
18
|
export { ALL_ENTRIES, ALL_TOOLS } from './tools';
|
|
19
19
|
export { STEAM_CAPSULE_GENERATOR_TOOL, steamCapsuleGenerator } from './tool/steamCapsuleGenerator';
|
|
20
20
|
export { STEAM_BBCODE_TRANSLATOR_TOOL, steamBbcodeTranslator } from './tool/steamBbcodeTranslator';
|
|
21
|
+
export { RETRO_SFX_GENERATOR_TOOL, retroSfxGenerator } from './tool/retroSfxGenerator';
|
|
22
|
+
export { PIXEL_ART_PALETTE_SWAPPER_TOOL, pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper';
|
|
21
23
|
|
|
22
24
|
export type { ToolLocaleContent as GamesToolLocaleContent } from './types';
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
type Color = { r: number; g: number; b: number; a: number };
|
|
6
|
+
type Declaration = { name: string; value: string };
|
|
7
|
+
type CssRule = { selector: string; declarations: Declaration[]; line: number };
|
|
8
|
+
type Theme = 'light' | 'dark';
|
|
9
|
+
type Variables = Record<string, string>;
|
|
10
|
+
|
|
11
|
+
const toolRoot = join(process.cwd(), 'src', 'tool');
|
|
12
|
+
const minimumTextContrast = 4.5;
|
|
13
|
+
const minimumReviewFontSizeRem = 0.75;
|
|
14
|
+
const themes: Theme[] = ['light', 'dark'];
|
|
15
|
+
|
|
16
|
+
function findFiles(directory: string, extensions: string[]): string[] {
|
|
17
|
+
const files: string[] = [];
|
|
18
|
+
|
|
19
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
20
|
+
const path = join(directory, entry.name);
|
|
21
|
+
if (entry.isDirectory()) files.push(...findFiles(path, extensions));
|
|
22
|
+
else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(path);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return files;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function relativePath(path: string): string {
|
|
29
|
+
return relative(process.cwd(), path).replace(/\\/g, '/');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lineAt(source: string, index: number): number {
|
|
33
|
+
return source.slice(0, index).split('\n').length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function removeComments(source: string): string {
|
|
37
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ' '));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cssSources(path: string): string[] {
|
|
41
|
+
const source = readFileSync(path, 'utf8');
|
|
42
|
+
if (path.endsWith('.css')) return [source];
|
|
43
|
+
|
|
44
|
+
return Array.from(source.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi), (match) => match[1])
|
|
45
|
+
.filter((style): style is string => style !== undefined);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseDeclarations(body: string): Declaration[] {
|
|
49
|
+
return body
|
|
50
|
+
.split(';')
|
|
51
|
+
.map((declaration) => {
|
|
52
|
+
const separator = declaration.indexOf(':');
|
|
53
|
+
if (separator === -1) return null;
|
|
54
|
+
return {
|
|
55
|
+
name: declaration.slice(0, separator).trim().toLowerCase(),
|
|
56
|
+
value: declaration.slice(separator + 1).trim(),
|
|
57
|
+
};
|
|
58
|
+
})
|
|
59
|
+
.filter((declaration): declaration is Declaration => declaration !== null && declaration.name.length > 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseRules(source: string): CssRule[] {
|
|
63
|
+
const css = removeComments(source);
|
|
64
|
+
const rules: CssRule[] = [];
|
|
65
|
+
const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
|
|
66
|
+
|
|
67
|
+
for (const match of css.matchAll(rulePattern)) {
|
|
68
|
+
const selector = match[1]?.trim();
|
|
69
|
+
const body = match[2];
|
|
70
|
+
if (selector && body !== undefined) {
|
|
71
|
+
const declarations = parseDeclarations(body);
|
|
72
|
+
if (declarations.length > 0) {
|
|
73
|
+
rules.push({ selector, declarations, line: lineAt(css, match.index ?? 0) });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return rules;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function channel(value: string): number | null {
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
if (trimmed.endsWith('%')) {
|
|
84
|
+
const percentage = Number.parseFloat(trimmed);
|
|
85
|
+
return Number.isFinite(percentage) ? (percentage / 100) * 255 : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const number = Number.parseFloat(trimmed);
|
|
89
|
+
return Number.isFinite(number) ? number : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function expandHex(hex: string): string {
|
|
93
|
+
return hex.length <= 4 ? hex.split('').map((part) => part + part).join('') : hex;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hexValues(expanded: string): number[] | null {
|
|
97
|
+
const values = expanded.match(/[\da-f]{2}/gi)?.map((part) => Number.parseInt(part, 16));
|
|
98
|
+
if (!values || values.length < 3) return null;
|
|
99
|
+
return values;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseHexColor(value: string): Color | null {
|
|
103
|
+
const hex = value.match(/^#([\da-f]{3,8})$/i)?.[1];
|
|
104
|
+
if (!hex) return null;
|
|
105
|
+
const expanded = expandHex(hex);
|
|
106
|
+
if (![6, 8].includes(expanded.length)) return null;
|
|
107
|
+
const values = hexValues(expanded);
|
|
108
|
+
if (!values) return null;
|
|
109
|
+
const [r, g, b, alpha] = values;
|
|
110
|
+
return { r: r!, g: g!, b: b!, a: alpha === undefined ? 1 : alpha / 255 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function rgbChannels(parts: string[]): [number, number, number] | null {
|
|
114
|
+
const [r, g, b] = parts.slice(0, 3).map(channel);
|
|
115
|
+
if ([r, g, b].some((value) => value === null || value === undefined)) return null;
|
|
116
|
+
return [r!, g!, b!];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function alphaValue(value: string | undefined): number | null {
|
|
120
|
+
if (value === undefined) return 1;
|
|
121
|
+
const alpha = Number.parseFloat(value);
|
|
122
|
+
return Number.isFinite(alpha) ? alpha : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseRgbParts(rgb: string): Color | null {
|
|
126
|
+
const parts = rgb.replace('/', ',').split(',').map((part) => part.trim());
|
|
127
|
+
if (parts.length < 3) return null;
|
|
128
|
+
const channels = rgbChannels(parts);
|
|
129
|
+
const alpha = alphaValue(parts[3]);
|
|
130
|
+
if (!channels || alpha === null) return null;
|
|
131
|
+
const [r, g, b] = channels;
|
|
132
|
+
return { r, g, b, a: alpha };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseRgbColor(value: string): Color | null {
|
|
136
|
+
const rgb = value.match(/^rgba?\(([^)]+)\)$/)?.[1];
|
|
137
|
+
return rgb ? parseRgbParts(rgb) : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseColor(value: string): Color | null {
|
|
141
|
+
const trimmed = value.trim().toLowerCase();
|
|
142
|
+
const fallback = trimmed.match(/^var\([^,]+,\s*(.+)\)$/)?.[1];
|
|
143
|
+
if (fallback) return parseColor(fallback);
|
|
144
|
+
if (trimmed === 'white') return { r: 255, g: 255, b: 255, a: 1 };
|
|
145
|
+
if (trimmed === 'black') return { r: 0, g: 0, b: 0, a: 1 };
|
|
146
|
+
if (trimmed === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
|
|
147
|
+
return parseHexColor(trimmed) ?? parseRgbColor(trimmed);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function composite(foreground: Color, background: Color): Color | null {
|
|
151
|
+
if (background.a < 1) return null;
|
|
152
|
+
if (foreground.a >= 1) return foreground;
|
|
153
|
+
const alpha = foreground.a;
|
|
154
|
+
return {
|
|
155
|
+
r: foreground.r * alpha + background.r * (1 - alpha),
|
|
156
|
+
g: foreground.g * alpha + background.g * (1 - alpha),
|
|
157
|
+
b: foreground.b * alpha + background.b * (1 - alpha),
|
|
158
|
+
a: 1,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function relativeLuminance(color: Color): number {
|
|
163
|
+
const linear = [color.r, color.g, color.b].map((value) => {
|
|
164
|
+
const channelValue = value / 255;
|
|
165
|
+
return channelValue <= 0.03928
|
|
166
|
+
? channelValue / 12.92
|
|
167
|
+
: ((channelValue + 0.055) / 1.055) ** 2.4;
|
|
168
|
+
});
|
|
169
|
+
return 0.2126 * linear[0]! + 0.7152 * linear[1]! + 0.0722 * linear[2]!;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function contrastRatio(foreground: Color, background: Color): number | null {
|
|
173
|
+
const compositedForeground = composite(foreground, background);
|
|
174
|
+
if (!compositedForeground) return null;
|
|
175
|
+
const foregroundLuminance = relativeLuminance(compositedForeground);
|
|
176
|
+
const backgroundLuminance = relativeLuminance(background);
|
|
177
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05)
|
|
178
|
+
/ (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function lastDeclaration(declarations: Declaration[], names: string[]): Declaration | undefined {
|
|
182
|
+
return [...declarations].reverse().find((declaration) => names.includes(declaration.name));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function simpleBackground(value: string): Color | null {
|
|
186
|
+
if (value.includes('gradient') || value.includes('url(') || value.includes('color-mix(')) return null;
|
|
187
|
+
return parseColor(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isThemeSelector(selector: string, theme: Theme): boolean {
|
|
191
|
+
return selector.includes(`.theme-${theme}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isActiveRule(selector: string, theme: Theme): boolean {
|
|
195
|
+
const otherTheme: Theme = theme === 'dark' ? 'light' : 'dark';
|
|
196
|
+
return !isThemeSelector(selector, otherTheme);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function addVariables(target: Variables, rule: CssRule): void {
|
|
200
|
+
rule.declarations
|
|
201
|
+
.filter((declaration) => declaration.name.startsWith('--'))
|
|
202
|
+
.forEach((declaration) => { target[declaration.name] = declaration.value; });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function collectVariables(rules: CssRule[], theme: Theme): Variables {
|
|
206
|
+
const variables: Variables = {};
|
|
207
|
+
rules.filter((rule) => !isThemeSelector(rule.selector, 'dark') && !isThemeSelector(rule.selector, 'light'))
|
|
208
|
+
.forEach((rule) => addVariables(variables, rule));
|
|
209
|
+
rules.filter((rule) => isThemeSelector(rule.selector, theme))
|
|
210
|
+
.forEach((rule) => addVariables(variables, rule));
|
|
211
|
+
return variables;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveCssValue(value: string, variables: Variables): string {
|
|
215
|
+
let resolved = value;
|
|
216
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
217
|
+
const next = resolved.replace(/var\(\s*(--[\w-]+)(?:\s*,\s*([^()]+))?\s*\)/g, (_, name, fallback) => variables[name] ?? fallback ?? '');
|
|
218
|
+
if (next === resolved) return resolved;
|
|
219
|
+
resolved = next;
|
|
220
|
+
}
|
|
221
|
+
return resolved;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function contrastFinding(path: string, rule: CssRule, theme: Theme, variables: Variables): string | null {
|
|
225
|
+
const foreground = lastDeclaration(rule.declarations, ['color']);
|
|
226
|
+
const background = lastDeclaration(rule.declarations, ['background-color', 'background']);
|
|
227
|
+
const foregroundColor = foreground ? parseColor(resolveCssValue(foreground.value, variables)) : null;
|
|
228
|
+
const backgroundColor = background ? simpleBackground(resolveCssValue(background.value, variables)) : null;
|
|
229
|
+
const ratio = foregroundColor && backgroundColor ? contrastRatio(foregroundColor, backgroundColor) : null;
|
|
230
|
+
return ratio !== null && ratio < minimumTextContrast
|
|
231
|
+
? `[A11Y-06][${theme}] ${relativePath(path)}:${rule.line} ${rule.selector} — ${ratio.toFixed(2)}:1 `
|
|
232
|
+
+ `(mínimo ${minimumTextContrast}:1)`
|
|
233
|
+
: null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fontSizeFinding(path: string, rule: CssRule): string | null {
|
|
237
|
+
const rem = lastDeclaration(rule.declarations, ['font-size'])?.value.match(/^(-?(?:\d+\.?\d*|\.\d+))rem$/i)?.[1];
|
|
238
|
+
if (!rem || Number.parseFloat(rem) >= minimumReviewFontSizeRem || /(svg|icon|logo|decorative|sr-only)/i.test(rule.selector)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return `[A11Y-06] ${relativePath(path)}:${rule.line} ${rule.selector} — font-size ${rem}rem `
|
|
242
|
+
+ `(revisar por debajo de ${minimumReviewFontSizeRem}rem)`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function inspectSource(path: string, source: string): { contrastFailures: string[]; fontSizeCandidates: string[] } {
|
|
246
|
+
const rules = parseRules(source);
|
|
247
|
+
const contrastFailures = themes.flatMap((theme) => {
|
|
248
|
+
const variables = collectVariables(rules, theme);
|
|
249
|
+
return rules
|
|
250
|
+
.filter((rule) => isActiveRule(rule.selector, theme))
|
|
251
|
+
.map((rule) => contrastFinding(path, rule, theme, variables))
|
|
252
|
+
.filter((finding): finding is string => finding !== null);
|
|
253
|
+
});
|
|
254
|
+
const fontSizeCandidates = rules
|
|
255
|
+
.map((rule) => fontSizeFinding(path, rule))
|
|
256
|
+
.filter((finding): finding is string => finding !== null);
|
|
257
|
+
return {
|
|
258
|
+
contrastFailures,
|
|
259
|
+
fontSizeCandidates,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function inspectStyles(): { contrastFailures: string[]; fontSizeCandidates: string[] } {
|
|
264
|
+
const findings = findFiles(toolRoot, ['.css', '.astro'])
|
|
265
|
+
.flatMap((path) => cssSources(path).map((source) => inspectSource(path, source)));
|
|
266
|
+
return {
|
|
267
|
+
contrastFailures: findings.flatMap((finding) => finding.contrastFailures),
|
|
268
|
+
fontSizeCandidates: findings.flatMap((finding) => finding.fontSizeCandidates),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
describe('QA: contraste y legibilidad de las tools', () => {
|
|
273
|
+
it('calcula ratios WCAG conocidos correctamente', () => {
|
|
274
|
+
expect(contrastRatio(parseColor('#000')!, parseColor('#fff')!)).toBeCloseTo(21, 5);
|
|
275
|
+
expect(contrastRatio(parseColor('#777')!, parseColor('#fff')!)).toBeCloseTo(4.478, 2);
|
|
276
|
+
expect(contrastRatio(parseColor('rgba(0, 0, 0, 0.5)')!, parseColor('#fff')!)).toBeCloseTo(3.98, 2);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('no contiene pares de texto y fondo explícitos por debajo de 4.5:1', () => {
|
|
280
|
+
const { contrastFailures } = inspectStyles();
|
|
281
|
+
expect(contrastFailures, `Contrastes insuficientes:\n${contrastFailures.join('\n')}`).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('no usa texto por debajo de 0.75rem fuera de elementos decorativos', () => {
|
|
285
|
+
const { fontSizeCandidates } = inspectStyles();
|
|
286
|
+
expect(
|
|
287
|
+
fontSizeCandidates,
|
|
288
|
+
'Candidatos de legibilidad. El umbral es una política de revisión del repositorio, no una validación completa de WCAG.',
|
|
289
|
+
).toEqual([]);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
3
|
|
|
4
4
|
describe('Locale Completeness Validation', () => {
|
|
5
|
-
it('
|
|
6
|
-
expect(ALL_TOOLS.length).toBe(
|
|
5
|
+
it('nine tools are registered', () => {
|
|
6
|
+
expect(ALL_TOOLS.length).toBe(9);
|
|
7
7
|
});
|
|
8
8
|
});
|
|
@@ -4,8 +4,8 @@ import { gamesCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 9 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(9);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('gamesCategory should be defined', () => {
|
|
@@ -113,9 +113,9 @@
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
.igt-chip:hover {
|
|
116
|
-
background: var(--color-primary, #
|
|
116
|
+
background: var(--color-primary, #4f46e5);
|
|
117
117
|
color: var(--text-inverse, #fff);
|
|
118
|
-
border-color: var(--color-primary, #
|
|
118
|
+
border-color: var(--color-primary, #4f46e5);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
.igt-chip.active {
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
padding: 0.75rem 1.25rem;
|
|
196
196
|
border-radius: 8px;
|
|
197
197
|
border: none;
|
|
198
|
-
background: var(--color-primary, #
|
|
198
|
+
background: var(--color-primary, #4f46e5);
|
|
199
199
|
color: var(--text-inverse, #fff);
|
|
200
200
|
font-weight: 600;
|
|
201
201
|
cursor: pointer;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
--ls-panel-soft: #f4f7fa;
|
|
7
7
|
--ls-accent: #e56a47;
|
|
8
8
|
--ls-accent-dark: #bc4d30;
|
|
9
|
+
--ls-on-accent: #17202f;
|
|
9
10
|
--ls-cyan: #19a6a0;
|
|
10
11
|
--ls-yellow: #e7a633;
|
|
11
12
|
--ls-red: #ce4f61;
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
--ls-panel-soft: #172232;
|
|
25
26
|
--ls-accent: #f1845d;
|
|
26
27
|
--ls-accent-dark: #da6744;
|
|
28
|
+
--ls-on-accent: #111926;
|
|
27
29
|
--ls-cyan: #42c3b8;
|
|
28
30
|
--ls-yellow: #efbd57;
|
|
29
31
|
--ls-red: #f07889;
|
|
@@ -73,7 +75,7 @@
|
|
|
73
75
|
.ls-kicker,
|
|
74
76
|
.ls-section-eyebrow {
|
|
75
77
|
color: var(--ls-accent);
|
|
76
|
-
font-size: 0.
|
|
78
|
+
font-size: 0.75rem;
|
|
77
79
|
font-weight: 800;
|
|
78
80
|
letter-spacing: 0.14em;
|
|
79
81
|
}
|
|
@@ -101,7 +103,7 @@
|
|
|
101
103
|
|
|
102
104
|
.ls-format-label {
|
|
103
105
|
color: var(--ls-muted);
|
|
104
|
-
font-size: 0.
|
|
106
|
+
font-size: 0.75rem;
|
|
105
107
|
font-weight: 700;
|
|
106
108
|
margin-right: auto;
|
|
107
109
|
text-transform: uppercase;
|
|
@@ -162,7 +164,7 @@
|
|
|
162
164
|
.ls-drop-mark span {
|
|
163
165
|
border: 1px solid var(--ls-accent);
|
|
164
166
|
color: var(--ls-accent-dark);
|
|
165
|
-
font-size: 0.
|
|
167
|
+
font-size: 0.75rem;
|
|
166
168
|
font-weight: 900;
|
|
167
169
|
letter-spacing: 0.08em;
|
|
168
170
|
padding: 0.22rem 0.36rem;
|
|
@@ -170,7 +172,7 @@
|
|
|
170
172
|
|
|
171
173
|
.ls-drop-mark span:last-child {
|
|
172
174
|
background: var(--ls-accent);
|
|
173
|
-
color: var(--ls-
|
|
175
|
+
color: var(--ls-on-accent);
|
|
174
176
|
transform: translateX(0.4rem);
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -189,7 +191,7 @@
|
|
|
189
191
|
|
|
190
192
|
.ls-file-type-note {
|
|
191
193
|
color: var(--ls-cyan);
|
|
192
|
-
font-size: 0.
|
|
194
|
+
font-size: 0.75rem;
|
|
193
195
|
font-weight: 700;
|
|
194
196
|
}
|
|
195
197
|
|
|
@@ -219,7 +221,7 @@
|
|
|
219
221
|
|
|
220
222
|
.ls-button-primary {
|
|
221
223
|
background: var(--ls-accent);
|
|
222
|
-
color: var(--ls-
|
|
224
|
+
color: var(--ls-on-accent);
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
.ls-button-primary:hover:not(:disabled) {
|
|
@@ -277,7 +279,7 @@
|
|
|
277
279
|
|
|
278
280
|
.ls-section-index {
|
|
279
281
|
color: var(--ls-accent);
|
|
280
|
-
font-size: 0.
|
|
282
|
+
font-size: 0.75rem;
|
|
281
283
|
font-weight: 900;
|
|
282
284
|
padding-top: 0.16rem;
|
|
283
285
|
}
|
|
@@ -329,7 +331,7 @@
|
|
|
329
331
|
|
|
330
332
|
.ls-health-card span:last-child {
|
|
331
333
|
color: var(--ls-muted);
|
|
332
|
-
font-size: 0.
|
|
334
|
+
font-size: 0.75rem;
|
|
333
335
|
margin-top: 0.22rem;
|
|
334
336
|
}
|
|
335
337
|
|
|
@@ -435,14 +437,14 @@
|
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
.ls-issue-copy strong {
|
|
438
|
-
font-size: 0.
|
|
440
|
+
font-size: 0.75rem;
|
|
439
441
|
}
|
|
440
442
|
|
|
441
443
|
.ls-issue-copy span,
|
|
442
444
|
.ls-issue-more,
|
|
443
445
|
.ls-no-issues {
|
|
444
446
|
color: var(--ls-muted);
|
|
445
|
-
font-size: 0.
|
|
447
|
+
font-size: 0.75rem;
|
|
446
448
|
}
|
|
447
449
|
|
|
448
450
|
.ls-issue-more {
|
|
@@ -491,7 +493,7 @@
|
|
|
491
493
|
}
|
|
492
494
|
|
|
493
495
|
.ls-empty-state span:last-child {
|
|
494
|
-
font-size: 0.
|
|
496
|
+
font-size: 0.75rem;
|
|
495
497
|
}
|
|
496
498
|
|
|
497
499
|
.ls-empty-grid {
|
|
@@ -505,7 +507,7 @@
|
|
|
505
507
|
|
|
506
508
|
.ls-table {
|
|
507
509
|
border-collapse: collapse;
|
|
508
|
-
font-size: 0.
|
|
510
|
+
font-size: 0.75rem;
|
|
509
511
|
min-width: 100%;
|
|
510
512
|
table-layout: fixed;
|
|
511
513
|
}
|
|
@@ -524,7 +526,7 @@
|
|
|
524
526
|
.ls-table th {
|
|
525
527
|
background: color-mix(in srgb, var(--ls-accent) 8%, var(--ls-panel));
|
|
526
528
|
color: var(--ls-accent-dark);
|
|
527
|
-
font-size: 0.
|
|
529
|
+
font-size: 0.75rem;
|
|
528
530
|
letter-spacing: 0.05em;
|
|
529
531
|
position: sticky;
|
|
530
532
|
text-transform: uppercase;
|
|
@@ -544,7 +546,7 @@
|
|
|
544
546
|
.ls-preview-note {
|
|
545
547
|
color: var(--ls-muted);
|
|
546
548
|
display: block;
|
|
547
|
-
font-size: 0.
|
|
549
|
+
font-size: 0.75rem;
|
|
548
550
|
padding: 0.6rem;
|
|
549
551
|
text-align: center;
|
|
550
552
|
}
|
|
@@ -568,7 +570,7 @@
|
|
|
568
570
|
|
|
569
571
|
.ls-live-status {
|
|
570
572
|
color: var(--ls-cyan);
|
|
571
|
-
font-size: 0.
|
|
573
|
+
font-size: 0.75rem;
|
|
572
574
|
min-height: 1rem;
|
|
573
575
|
padding: 0.35rem 0.2rem;
|
|
574
576
|
}
|