@jjlmoya/utils-games-development 1.54.0 → 1.55.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 +4 -1
- package/src/index.ts +1 -0
- package/src/tests/contrast_legibility.test.ts +245 -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 +13 -13
- package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
- package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
- package/src/tool/retroSfxGenerator/client.ts +256 -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 +40 -0
- package/src/tool/retroSfxGenerator/logic.ts +214 -0
- package/src/tool/retroSfxGenerator/retro-sfx-generator.css +429 -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 +2 -0
package/package.json
CHANGED
package/src/entries.ts
CHANGED
|
@@ -12,6 +12,8 @@ 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';
|
|
15
17
|
export { gamesCategory } from './category';
|
|
16
18
|
import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
|
|
17
19
|
import { itchioGameTester } from './tool/itchioGameTester/entry';
|
|
@@ -19,5 +21,6 @@ import { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
|
|
|
19
21
|
import { audioLoopPointFinder } from './tool/audioLoopPointFinder/entry';
|
|
20
22
|
import { saveFileEditor } from './tool/saveFileEditor/entry';
|
|
21
23
|
import { localizationSanitizer } from './tool/localizationSanitizer/entry';
|
|
24
|
+
import { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
|
|
22
25
|
|
|
23
|
-
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer];
|
|
26
|
+
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, retroSfxGenerator];
|
package/src/index.ts
CHANGED
|
@@ -18,5 +18,6 @@ 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';
|
|
21
22
|
|
|
22
23
|
export type { ToolLocaleContent as GamesToolLocaleContent } from './types';
|
|
@@ -0,0 +1,245 @@
|
|
|
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
|
+
|
|
9
|
+
const toolRoot = join(process.cwd(), 'src', 'tool');
|
|
10
|
+
const minimumTextContrast = 4.5;
|
|
11
|
+
const minimumReviewFontSizeRem = 0.75;
|
|
12
|
+
|
|
13
|
+
function findFiles(directory: string, extensions: string[]): string[] {
|
|
14
|
+
const files: string[] = [];
|
|
15
|
+
|
|
16
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
17
|
+
const path = join(directory, entry.name);
|
|
18
|
+
if (entry.isDirectory()) files.push(...findFiles(path, extensions));
|
|
19
|
+
else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(path);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function relativePath(path: string): string {
|
|
26
|
+
return relative(process.cwd(), path).replace(/\\/g, '/');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function lineAt(source: string, index: number): number {
|
|
30
|
+
return source.slice(0, index).split('\n').length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function removeComments(source: string): string {
|
|
34
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ' '));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cssSources(path: string): string[] {
|
|
38
|
+
const source = readFileSync(path, 'utf8');
|
|
39
|
+
if (path.endsWith('.css')) return [source];
|
|
40
|
+
|
|
41
|
+
return Array.from(source.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi), (match) => match[1])
|
|
42
|
+
.filter((style): style is string => style !== undefined);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDeclarations(body: string): Declaration[] {
|
|
46
|
+
return body
|
|
47
|
+
.split(';')
|
|
48
|
+
.map((declaration) => {
|
|
49
|
+
const separator = declaration.indexOf(':');
|
|
50
|
+
if (separator === -1) return null;
|
|
51
|
+
return {
|
|
52
|
+
name: declaration.slice(0, separator).trim().toLowerCase(),
|
|
53
|
+
value: declaration.slice(separator + 1).trim(),
|
|
54
|
+
};
|
|
55
|
+
})
|
|
56
|
+
.filter((declaration): declaration is Declaration => declaration !== null && declaration.name.length > 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseRules(source: string): CssRule[] {
|
|
60
|
+
const css = removeComments(source);
|
|
61
|
+
const rules: CssRule[] = [];
|
|
62
|
+
const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
|
|
63
|
+
|
|
64
|
+
for (const match of css.matchAll(rulePattern)) {
|
|
65
|
+
const selector = match[1]?.trim();
|
|
66
|
+
const body = match[2];
|
|
67
|
+
if (selector && body !== undefined) {
|
|
68
|
+
const declarations = parseDeclarations(body);
|
|
69
|
+
if (declarations.length > 0) {
|
|
70
|
+
rules.push({ selector, declarations, line: lineAt(css, match.index ?? 0) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return rules;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function channel(value: string): number | null {
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
if (trimmed.endsWith('%')) {
|
|
81
|
+
const percentage = Number.parseFloat(trimmed);
|
|
82
|
+
return Number.isFinite(percentage) ? (percentage / 100) * 255 : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const number = Number.parseFloat(trimmed);
|
|
86
|
+
return Number.isFinite(number) ? number : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function expandHex(hex: string): string {
|
|
90
|
+
return hex.length <= 4 ? hex.split('').map((part) => part + part).join('') : hex;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hexValues(expanded: string): number[] | null {
|
|
94
|
+
const values = expanded.match(/[\da-f]{2}/gi)?.map((part) => Number.parseInt(part, 16));
|
|
95
|
+
if (!values || values.length < 3) return null;
|
|
96
|
+
return values;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseHexColor(value: string): Color | null {
|
|
100
|
+
const hex = value.match(/^#([\da-f]{3,8})$/i)?.[1];
|
|
101
|
+
if (!hex) return null;
|
|
102
|
+
const expanded = expandHex(hex);
|
|
103
|
+
if (![6, 8].includes(expanded.length)) return null;
|
|
104
|
+
const values = hexValues(expanded);
|
|
105
|
+
if (!values) return null;
|
|
106
|
+
const [r, g, b, alpha] = values;
|
|
107
|
+
return { r: r!, g: g!, b: b!, a: alpha === undefined ? 1 : alpha / 255 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function rgbChannels(parts: string[]): [number, number, number] | null {
|
|
111
|
+
const [r, g, b] = parts.slice(0, 3).map(channel);
|
|
112
|
+
if ([r, g, b].some((value) => value === null || value === undefined)) return null;
|
|
113
|
+
return [r!, g!, b!];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function alphaValue(value: string | undefined): number | null {
|
|
117
|
+
if (value === undefined) return 1;
|
|
118
|
+
const alpha = Number.parseFloat(value);
|
|
119
|
+
return Number.isFinite(alpha) ? alpha : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseRgbParts(rgb: string): Color | null {
|
|
123
|
+
const parts = rgb.replace('/', ',').split(',').map((part) => part.trim());
|
|
124
|
+
if (parts.length < 3) return null;
|
|
125
|
+
const channels = rgbChannels(parts);
|
|
126
|
+
const alpha = alphaValue(parts[3]);
|
|
127
|
+
if (!channels || alpha === null) return null;
|
|
128
|
+
const [r, g, b] = channels;
|
|
129
|
+
return { r, g, b, a: alpha };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseRgbColor(value: string): Color | null {
|
|
133
|
+
const rgb = value.match(/^rgba?\(([^)]+)\)$/)?.[1];
|
|
134
|
+
return rgb ? parseRgbParts(rgb) : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseColor(value: string): Color | null {
|
|
138
|
+
const trimmed = value.trim().toLowerCase();
|
|
139
|
+
const fallback = trimmed.match(/^var\([^,]+,\s*(.+)\)$/)?.[1];
|
|
140
|
+
if (fallback) return parseColor(fallback);
|
|
141
|
+
if (trimmed === 'white') return { r: 255, g: 255, b: 255, a: 1 };
|
|
142
|
+
if (trimmed === 'black') return { r: 0, g: 0, b: 0, a: 1 };
|
|
143
|
+
if (trimmed === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
|
|
144
|
+
return parseHexColor(trimmed) ?? parseRgbColor(trimmed);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function composite(foreground: Color, background: Color): Color | null {
|
|
148
|
+
if (background.a < 1) return null;
|
|
149
|
+
if (foreground.a >= 1) return foreground;
|
|
150
|
+
const alpha = foreground.a;
|
|
151
|
+
return {
|
|
152
|
+
r: foreground.r * alpha + background.r * (1 - alpha),
|
|
153
|
+
g: foreground.g * alpha + background.g * (1 - alpha),
|
|
154
|
+
b: foreground.b * alpha + background.b * (1 - alpha),
|
|
155
|
+
a: 1,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function relativeLuminance(color: Color): number {
|
|
160
|
+
const linear = [color.r, color.g, color.b].map((value) => {
|
|
161
|
+
const channelValue = value / 255;
|
|
162
|
+
return channelValue <= 0.03928
|
|
163
|
+
? channelValue / 12.92
|
|
164
|
+
: ((channelValue + 0.055) / 1.055) ** 2.4;
|
|
165
|
+
});
|
|
166
|
+
return 0.2126 * linear[0]! + 0.7152 * linear[1]! + 0.0722 * linear[2]!;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function contrastRatio(foreground: Color, background: Color): number | null {
|
|
170
|
+
const compositedForeground = composite(foreground, background);
|
|
171
|
+
if (!compositedForeground) return null;
|
|
172
|
+
const foregroundLuminance = relativeLuminance(compositedForeground);
|
|
173
|
+
const backgroundLuminance = relativeLuminance(background);
|
|
174
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05)
|
|
175
|
+
/ (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function lastDeclaration(declarations: Declaration[], names: string[]): Declaration | undefined {
|
|
179
|
+
return [...declarations].reverse().find((declaration) => names.includes(declaration.name));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function simpleBackground(value: string): Color | null {
|
|
183
|
+
if (value.includes('gradient') || value.includes('url(') || value.includes('color-mix(')) return null;
|
|
184
|
+
return parseColor(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function contrastFinding(path: string, rule: CssRule): string | null {
|
|
188
|
+
const foreground = lastDeclaration(rule.declarations, ['color']);
|
|
189
|
+
const background = lastDeclaration(rule.declarations, ['background-color', 'background']);
|
|
190
|
+
const foregroundColor = foreground ? parseColor(foreground.value) : null;
|
|
191
|
+
const backgroundColor = background ? simpleBackground(background.value) : null;
|
|
192
|
+
const ratio = foregroundColor && backgroundColor ? contrastRatio(foregroundColor, backgroundColor) : null;
|
|
193
|
+
return ratio !== null && ratio < minimumTextContrast
|
|
194
|
+
? `[A11Y-06] ${relativePath(path)}:${rule.line} ${rule.selector} — ${ratio.toFixed(2)}:1 `
|
|
195
|
+
+ `(mínimo ${minimumTextContrast}:1)`
|
|
196
|
+
: null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function fontSizeFinding(path: string, rule: CssRule): string | null {
|
|
200
|
+
const rem = lastDeclaration(rule.declarations, ['font-size'])?.value.match(/^(-?(?:\d+\.?\d*|\.\d+))rem$/i)?.[1];
|
|
201
|
+
if (!rem || Number.parseFloat(rem) >= minimumReviewFontSizeRem || /(svg|icon|logo|decorative|sr-only)/i.test(rule.selector)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return `[A11Y-06] ${relativePath(path)}:${rule.line} ${rule.selector} — font-size ${rem}rem `
|
|
205
|
+
+ `(revisar por debajo de ${minimumReviewFontSizeRem}rem)`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function inspectRule(path: string, rule: CssRule): { contrastFailures: string[]; fontSizeCandidates: string[] } {
|
|
209
|
+
const contrast = contrastFinding(path, rule);
|
|
210
|
+
const fontSize = fontSizeFinding(path, rule);
|
|
211
|
+
return {
|
|
212
|
+
contrastFailures: contrast ? [contrast] : [],
|
|
213
|
+
fontSizeCandidates: fontSize ? [fontSize] : [],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function inspectStyles(): { contrastFailures: string[]; fontSizeCandidates: string[] } {
|
|
218
|
+
const findings = findFiles(toolRoot, ['.css', '.astro'])
|
|
219
|
+
.flatMap((path) => cssSources(path).flatMap((source) => parseRules(source).map((rule) => inspectRule(path, rule))));
|
|
220
|
+
return {
|
|
221
|
+
contrastFailures: findings.flatMap((finding) => finding.contrastFailures),
|
|
222
|
+
fontSizeCandidates: findings.flatMap((finding) => finding.fontSizeCandidates),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
describe('QA: contraste y legibilidad de las tools', () => {
|
|
227
|
+
it('calcula ratios WCAG conocidos correctamente', () => {
|
|
228
|
+
expect(contrastRatio(parseColor('#000')!, parseColor('#fff')!)).toBeCloseTo(21, 5);
|
|
229
|
+
expect(contrastRatio(parseColor('#777')!, parseColor('#fff')!)).toBeCloseTo(4.478, 2);
|
|
230
|
+
expect(contrastRatio(parseColor('rgba(0, 0, 0, 0.5)')!, parseColor('#fff')!)).toBeCloseTo(3.98, 2);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('no contiene pares de texto y fondo explícitos por debajo de 4.5:1', () => {
|
|
234
|
+
const { contrastFailures } = inspectStyles();
|
|
235
|
+
expect(contrastFailures, `Contrastes insuficientes:\n${contrastFailures.join('\n')}`).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('no usa texto por debajo de 0.75rem fuera de elementos decorativos', () => {
|
|
239
|
+
const { fontSizeCandidates } = inspectStyles();
|
|
240
|
+
expect(
|
|
241
|
+
fontSizeCandidates,
|
|
242
|
+
'Candidatos de legibilidad. El umbral es una política de revisión del repositorio, no una validación completa de WCAG.',
|
|
243
|
+
).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -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('eight tools are registered', () => {
|
|
6
|
+
expect(ALL_TOOLS.length).toBe(8);
|
|
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 8 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(8);
|
|
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;
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
.ls-kicker,
|
|
74
74
|
.ls-section-eyebrow {
|
|
75
75
|
color: var(--ls-accent);
|
|
76
|
-
font-size: 0.
|
|
76
|
+
font-size: 0.75rem;
|
|
77
77
|
font-weight: 800;
|
|
78
78
|
letter-spacing: 0.14em;
|
|
79
79
|
}
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
|
|
102
102
|
.ls-format-label {
|
|
103
103
|
color: var(--ls-muted);
|
|
104
|
-
font-size: 0.
|
|
104
|
+
font-size: 0.75rem;
|
|
105
105
|
font-weight: 700;
|
|
106
106
|
margin-right: auto;
|
|
107
107
|
text-transform: uppercase;
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
.ls-drop-mark span {
|
|
163
163
|
border: 1px solid var(--ls-accent);
|
|
164
164
|
color: var(--ls-accent-dark);
|
|
165
|
-
font-size: 0.
|
|
165
|
+
font-size: 0.75rem;
|
|
166
166
|
font-weight: 900;
|
|
167
167
|
letter-spacing: 0.08em;
|
|
168
168
|
padding: 0.22rem 0.36rem;
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
|
|
190
190
|
.ls-file-type-note {
|
|
191
191
|
color: var(--ls-cyan);
|
|
192
|
-
font-size: 0.
|
|
192
|
+
font-size: 0.75rem;
|
|
193
193
|
font-weight: 700;
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
|
|
278
278
|
.ls-section-index {
|
|
279
279
|
color: var(--ls-accent);
|
|
280
|
-
font-size: 0.
|
|
280
|
+
font-size: 0.75rem;
|
|
281
281
|
font-weight: 900;
|
|
282
282
|
padding-top: 0.16rem;
|
|
283
283
|
}
|
|
@@ -329,7 +329,7 @@
|
|
|
329
329
|
|
|
330
330
|
.ls-health-card span:last-child {
|
|
331
331
|
color: var(--ls-muted);
|
|
332
|
-
font-size: 0.
|
|
332
|
+
font-size: 0.75rem;
|
|
333
333
|
margin-top: 0.22rem;
|
|
334
334
|
}
|
|
335
335
|
|
|
@@ -435,14 +435,14 @@
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
.ls-issue-copy strong {
|
|
438
|
-
font-size: 0.
|
|
438
|
+
font-size: 0.75rem;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
.ls-issue-copy span,
|
|
442
442
|
.ls-issue-more,
|
|
443
443
|
.ls-no-issues {
|
|
444
444
|
color: var(--ls-muted);
|
|
445
|
-
font-size: 0.
|
|
445
|
+
font-size: 0.75rem;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
.ls-issue-more {
|
|
@@ -491,7 +491,7 @@
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
.ls-empty-state span:last-child {
|
|
494
|
-
font-size: 0.
|
|
494
|
+
font-size: 0.75rem;
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
.ls-empty-grid {
|
|
@@ -505,7 +505,7 @@
|
|
|
505
505
|
|
|
506
506
|
.ls-table {
|
|
507
507
|
border-collapse: collapse;
|
|
508
|
-
font-size: 0.
|
|
508
|
+
font-size: 0.75rem;
|
|
509
509
|
min-width: 100%;
|
|
510
510
|
table-layout: fixed;
|
|
511
511
|
}
|
|
@@ -524,7 +524,7 @@
|
|
|
524
524
|
.ls-table th {
|
|
525
525
|
background: color-mix(in srgb, var(--ls-accent) 8%, var(--ls-panel));
|
|
526
526
|
color: var(--ls-accent-dark);
|
|
527
|
-
font-size: 0.
|
|
527
|
+
font-size: 0.75rem;
|
|
528
528
|
letter-spacing: 0.05em;
|
|
529
529
|
position: sticky;
|
|
530
530
|
text-transform: uppercase;
|
|
@@ -544,7 +544,7 @@
|
|
|
544
544
|
.ls-preview-note {
|
|
545
545
|
color: var(--ls-muted);
|
|
546
546
|
display: block;
|
|
547
|
-
font-size: 0.
|
|
547
|
+
font-size: 0.75rem;
|
|
548
548
|
padding: 0.6rem;
|
|
549
549
|
text-align: center;
|
|
550
550
|
}
|
|
@@ -568,7 +568,7 @@
|
|
|
568
568
|
|
|
569
569
|
.ls-live-status {
|
|
570
570
|
color: var(--ls-cyan);
|
|
571
|
-
font-size: 0.
|
|
571
|
+
font-size: 0.75rem;
|
|
572
572
|
min-height: 1rem;
|
|
573
573
|
padding: 0.35rem 0.2rem;
|
|
574
574
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliographyEntries: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'W3C Web Audio API Recommendation',
|
|
6
|
+
url: 'https://www.w3.org/TR/webaudio/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'MDN OscillatorNode Web API Reference',
|
|
10
|
+
url: 'https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Microsoft RIFF WAVE Audio File Format',
|
|
14
|
+
url: 'https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html',
|
|
15
|
+
},
|
|
16
|
+
];
|