@jjlmoya/utils-games-development 1.53.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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +6 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/contrast_legibility.test.ts +245 -0
  6. package/src/tests/locale_completeness.test.ts +2 -2
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
  9. package/src/tool/localizationSanitizer/localization-sanitizer.css +13 -13
  10. package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
  11. package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
  12. package/src/tool/retroSfxGenerator/client.ts +256 -0
  13. package/src/tool/retroSfxGenerator/component.astro +146 -0
  14. package/src/tool/retroSfxGenerator/entry.ts +27 -0
  15. package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
  16. package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
  17. package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
  18. package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
  19. package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
  20. package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
  21. package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
  22. package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
  23. package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
  24. package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
  25. package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
  26. package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
  27. package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
  28. package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
  29. package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
  30. package/src/tool/retroSfxGenerator/index.ts +11 -0
  31. package/src/tool/retroSfxGenerator/logic.test.ts +40 -0
  32. package/src/tool/retroSfxGenerator/logic.ts +214 -0
  33. package/src/tool/retroSfxGenerator/retro-sfx-generator.css +429 -0
  34. package/src/tool/retroSfxGenerator/seo.astro +15 -0
  35. package/src/tool/retroSfxGenerator/ui.ts +41 -0
  36. package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
  37. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
  38. package/src/tool/steamBbcodeTranslator/app-client.ts +125 -0
  39. package/src/tool/steamBbcodeTranslator/bbcode-parser.ts +110 -0
  40. package/src/tool/steamBbcodeTranslator/bibliography.astro +6 -0
  41. package/src/tool/steamBbcodeTranslator/bibliography.ts +14 -0
  42. package/src/tool/steamBbcodeTranslator/component.astro +83 -0
  43. package/src/tool/steamBbcodeTranslator/entry.ts +28 -0
  44. package/src/tool/steamBbcodeTranslator/html-parser.ts +42 -0
  45. package/src/tool/steamBbcodeTranslator/html-renderer.ts +71 -0
  46. package/src/tool/steamBbcodeTranslator/i18n/de.ts +207 -0
  47. package/src/tool/steamBbcodeTranslator/i18n/en.ts +236 -0
  48. package/src/tool/steamBbcodeTranslator/i18n/es.ts +207 -0
  49. package/src/tool/steamBbcodeTranslator/i18n/fr.ts +195 -0
  50. package/src/tool/steamBbcodeTranslator/i18n/id.ts +195 -0
  51. package/src/tool/steamBbcodeTranslator/i18n/it.ts +195 -0
  52. package/src/tool/steamBbcodeTranslator/i18n/ja.ts +195 -0
  53. package/src/tool/steamBbcodeTranslator/i18n/ko.ts +195 -0
  54. package/src/tool/steamBbcodeTranslator/i18n/nl.ts +195 -0
  55. package/src/tool/steamBbcodeTranslator/i18n/pl.ts +195 -0
  56. package/src/tool/steamBbcodeTranslator/i18n/pt.ts +195 -0
  57. package/src/tool/steamBbcodeTranslator/i18n/ru.ts +195 -0
  58. package/src/tool/steamBbcodeTranslator/i18n/sv.ts +195 -0
  59. package/src/tool/steamBbcodeTranslator/i18n/tr.ts +195 -0
  60. package/src/tool/steamBbcodeTranslator/i18n/zh.ts +195 -0
  61. package/src/tool/steamBbcodeTranslator/index.ts +12 -0
  62. package/src/tool/steamBbcodeTranslator/logic.test.ts +41 -0
  63. package/src/tool/steamBbcodeTranslator/logic.ts +96 -0
  64. package/src/tool/steamBbcodeTranslator/markdown-parser.ts +115 -0
  65. package/src/tool/steamBbcodeTranslator/markdown-renderer.ts +85 -0
  66. package/src/tool/steamBbcodeTranslator/seo.astro +15 -0
  67. package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +392 -0
  68. package/src/tool/steamBbcodeTranslator/storage.ts +30 -0
  69. package/src/tool/steamBbcodeTranslator/types.ts +36 -0
  70. package/src/tool/steamBbcodeTranslator/ui.ts +18 -0
  71. package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
  72. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-games-development",
3
- "version": "1.53.0",
3
+ "version": "1.55.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -3,10 +3,11 @@ import { steamCapsuleGenerator } from '../tool/steamCapsuleGenerator/entry';
3
3
  import { spriteSheetPacker } from '../tool/spriteSheetPacker/entry';
4
4
  import { audioLoopPointFinder } from '../tool/audioLoopPointFinder/entry';
5
5
  import { saveFileEditor } from '../tool/saveFileEditor/entry';
6
+ import { steamBbcodeTranslator } from '../tool/steamBbcodeTranslator/entry';
6
7
 
7
8
  export const gamesCategory: GamesCategoryEntry = {
8
9
  icon: 'mdi:gamepad-variant',
9
- tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor],
10
+ tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, steamBbcodeTranslator],
10
11
  i18n: {
11
12
  de: () => import('./i18n/de').then((m) => m.content),
12
13
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -10,6 +10,10 @@ export { saveFileEditor } from './tool/saveFileEditor/entry';
10
10
  export type { SaveFileEditorUI, SaveFileEditorLocaleContent } from './tool/saveFileEditor/entry';
11
11
  export { localizationSanitizer } from './tool/localizationSanitizer/entry';
12
12
  export type { LocalizationSanitizerUI, LocalizationSanitizerLocaleContent } from './tool/localizationSanitizer/entry';
13
+ export { steamBbcodeTranslator } from './tool/steamBbcodeTranslator/entry';
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';
13
17
  export { gamesCategory } from './category';
14
18
  import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
15
19
  import { itchioGameTester } from './tool/itchioGameTester/entry';
@@ -17,5 +21,6 @@ import { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
17
21
  import { audioLoopPointFinder } from './tool/audioLoopPointFinder/entry';
18
22
  import { saveFileEditor } from './tool/saveFileEditor/entry';
19
23
  import { localizationSanitizer } from './tool/localizationSanitizer/entry';
24
+ import { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
20
25
 
21
- 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
@@ -17,5 +17,7 @@ export type {
17
17
 
18
18
  export { ALL_ENTRIES, ALL_TOOLS } from './tools';
19
19
  export { STEAM_CAPSULE_GENERATOR_TOOL, steamCapsuleGenerator } from './tool/steamCapsuleGenerator';
20
+ export { STEAM_BBCODE_TRANSLATOR_TOOL, steamBbcodeTranslator } from './tool/steamBbcodeTranslator';
21
+ export { RETRO_SFX_GENERATOR_TOOL, retroSfxGenerator } from './tool/retroSfxGenerator';
20
22
 
21
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('five tools are registered', () => {
6
- expect(ALL_TOOLS.length).toBe(6);
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 5 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(6);
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, #6366f1);
116
+ background: var(--color-primary, #4f46e5);
117
117
  color: var(--text-inverse, #fff);
118
- border-color: var(--color-primary, #6366f1);
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, #6366f1);
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.68rem;
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.72rem;
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.58rem;
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.69rem;
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.72rem;
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.7rem;
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.7rem;
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.66rem;
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.73rem;
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.72rem;
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.65rem;
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.68rem;
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.72rem;
571
+ font-size: 0.75rem;
572
572
  min-height: 1rem;
573
573
  padding: 0.35rem 0.2rem;
574
574
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { bibliographyEntries } from './bibliography';
4
+ ---
5
+
6
+ <SharedBibliography links={bibliographyEntries} />
@@ -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
+ ];