@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.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +7 -1
  3. package/src/index.ts +2 -0
  4. package/src/tests/contrast_legibility.test.ts +291 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
  8. package/src/tool/localizationSanitizer/localization-sanitizer.css +17 -15
  9. package/src/tool/pixelArtPaletteSwapper/app-client.ts +229 -0
  10. package/src/tool/pixelArtPaletteSwapper/bibliography.astro +6 -0
  11. package/src/tool/pixelArtPaletteSwapper/bibliography.ts +16 -0
  12. package/src/tool/pixelArtPaletteSwapper/component.astro +153 -0
  13. package/src/tool/pixelArtPaletteSwapper/entry.ts +28 -0
  14. package/src/tool/pixelArtPaletteSwapper/i18n/de.ts +233 -0
  15. package/src/tool/pixelArtPaletteSwapper/i18n/en.ts +233 -0
  16. package/src/tool/pixelArtPaletteSwapper/i18n/es.ts +233 -0
  17. package/src/tool/pixelArtPaletteSwapper/i18n/fr.ts +233 -0
  18. package/src/tool/pixelArtPaletteSwapper/i18n/id.ts +233 -0
  19. package/src/tool/pixelArtPaletteSwapper/i18n/it.ts +233 -0
  20. package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +225 -0
  21. package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +225 -0
  22. package/src/tool/pixelArtPaletteSwapper/i18n/nl.ts +233 -0
  23. package/src/tool/pixelArtPaletteSwapper/i18n/pl.ts +233 -0
  24. package/src/tool/pixelArtPaletteSwapper/i18n/pt.ts +233 -0
  25. package/src/tool/pixelArtPaletteSwapper/i18n/ru.ts +233 -0
  26. package/src/tool/pixelArtPaletteSwapper/i18n/sv.ts +233 -0
  27. package/src/tool/pixelArtPaletteSwapper/i18n/tr.ts +233 -0
  28. package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +225 -0
  29. package/src/tool/pixelArtPaletteSwapper/index.ts +12 -0
  30. package/src/tool/pixelArtPaletteSwapper/logic.test.ts +43 -0
  31. package/src/tool/pixelArtPaletteSwapper/logic.ts +171 -0
  32. package/src/tool/pixelArtPaletteSwapper/pixel-art-palette-swapper.css +728 -0
  33. package/src/tool/pixelArtPaletteSwapper/seo.astro +15 -0
  34. package/src/tool/pixelArtPaletteSwapper/ui.ts +35 -0
  35. package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
  36. package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
  37. package/src/tool/retroSfxGenerator/client.ts +263 -0
  38. package/src/tool/retroSfxGenerator/component.astro +146 -0
  39. package/src/tool/retroSfxGenerator/entry.ts +27 -0
  40. package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
  41. package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
  42. package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
  43. package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
  44. package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
  45. package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
  46. package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
  47. package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
  48. package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
  49. package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
  50. package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
  51. package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
  52. package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
  53. package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
  54. package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
  55. package/src/tool/retroSfxGenerator/index.ts +11 -0
  56. package/src/tool/retroSfxGenerator/logic.test.ts +61 -0
  57. package/src/tool/retroSfxGenerator/logic.ts +214 -0
  58. package/src/tool/retroSfxGenerator/retro-sfx-generator.css +435 -0
  59. package/src/tool/retroSfxGenerator/seo.astro +15 -0
  60. package/src/tool/retroSfxGenerator/ui.ts +41 -0
  61. package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
  62. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
  63. package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +8 -8
  64. package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
  65. package/src/tools.ts +4 -0
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ calculateEndFrequency,
4
+ createDefaultParams,
5
+ encodeWav,
6
+ generateSfx,
7
+ getPresetParams,
8
+ updateSweep,
9
+ type SfxPreset,
10
+ type SfxWaveform,
11
+ } from './logic';
12
+
13
+ describe('retroSfxGenerator logic', () => {
14
+ it('creates deterministic preset parameters when random is injected', () => {
15
+ const first = getPresetParams('coin', () => 0.5);
16
+ const second = getPresetParams('coin', () => 0.5);
17
+ expect(first).toEqual(second);
18
+ expect(first.frequencyEnd).toBe(calculateEndFrequency(first.frequency, first.sweep));
19
+ });
20
+
21
+ it('keeps random variations inside the selected preset family', () => {
22
+ const families: Array<[SfxPreset, SfxWaveform]> = [
23
+ ['explosion', 'noise'],
24
+ ['laser', 'sawtooth'],
25
+ ['jump', 'square'],
26
+ ['coin', 'square'],
27
+ ['powerUp', 'triangle'],
28
+ ];
29
+
30
+ families.forEach(([preset, waveform]) => {
31
+ expect(getPresetParams(preset, () => 0.25).waveform).toBe(waveform);
32
+ });
33
+ });
34
+
35
+ it('uses the visible explosion preset as the default family', () => {
36
+ expect(createDefaultParams().waveform).toBe('noise');
37
+ });
38
+
39
+ it('maps sweep to a bounded end frequency', () => {
40
+ expect(calculateEndFrequency(440, -1)).toBeGreaterThanOrEqual(40);
41
+ expect(calculateEndFrequency(440, 1)).toBeLessThanOrEqual(12000);
42
+ expect(updateSweep(getPresetParams('laser', () => 0.5), 0.25).frequencyEnd).toBeGreaterThan(440);
43
+ });
44
+
45
+ it('generates samples within the PCM range and expected duration', () => {
46
+ const params = getPresetParams('explosion', () => 0.5);
47
+ const generated = generateSfx(params, 8000);
48
+ expect(generated.samples.length).toBeGreaterThan(0);
49
+ expect(generated.duration).toBeCloseTo(params.duration, 2);
50
+ expect(Math.max(...generated.samples)).toBeLessThanOrEqual(1);
51
+ expect(Math.min(...generated.samples)).toBeGreaterThanOrEqual(-1);
52
+ });
53
+
54
+ it('encodes a mono 16-bit WAV with a valid RIFF header', () => {
55
+ const wav = encodeWav(new Float32Array([0, 0.5, -0.5, 0]), 44100);
56
+ expect(wav.length).toBe(52);
57
+ expect(String.fromCharCode(...wav.slice(0, 4))).toBe('RIFF');
58
+ expect(String.fromCharCode(...wav.slice(8, 12))).toBe('WAVE');
59
+ expect(new DataView(wav.buffer).getUint32(40, true)).toBe(8);
60
+ });
61
+ });
@@ -0,0 +1,214 @@
1
+ export type SfxWaveform = 'square' | 'sawtooth' | 'sine' | 'triangle' | 'noise';
2
+
3
+ export type SfxPreset = 'explosion' | 'laser' | 'jump' | 'coin' | 'powerUp';
4
+
5
+ export interface SfxParams {
6
+ waveform: SfxWaveform;
7
+ frequency: number;
8
+ frequencyEnd: number;
9
+ duration: number;
10
+ decay: number;
11
+ sweep: number;
12
+ vibrato: number;
13
+ lowpass: number;
14
+ highpass: number;
15
+ noiseMix: number;
16
+ }
17
+
18
+ export interface GeneratedSfx {
19
+ samples: Float32Array;
20
+ sampleRate: number;
21
+ duration: number;
22
+ }
23
+
24
+ export const MIN_SAMPLE_RATE = 8000;
25
+ export const DEFAULT_SAMPLE_RATE = 44100;
26
+
27
+ const PRESET_RANGES: Record<SfxPreset, Omit<SfxParams, 'frequencyEnd'>> = {
28
+ explosion: {
29
+ waveform: 'noise',
30
+ frequency: 180,
31
+ duration: 0.65,
32
+ decay: 0.84,
33
+ sweep: -0.55,
34
+ vibrato: 0.08,
35
+ lowpass: 3600,
36
+ highpass: 70,
37
+ noiseMix: 0.9,
38
+ },
39
+ laser: {
40
+ waveform: 'sawtooth',
41
+ frequency: 1200,
42
+ duration: 0.32,
43
+ decay: 0.56,
44
+ sweep: -0.7,
45
+ vibrato: 0.03,
46
+ lowpass: 9000,
47
+ highpass: 180,
48
+ noiseMix: 0.04,
49
+ },
50
+ jump: {
51
+ waveform: 'square',
52
+ frequency: 260,
53
+ duration: 0.42,
54
+ decay: 0.35,
55
+ sweep: 0.75,
56
+ vibrato: 0.06,
57
+ lowpass: 7600,
58
+ highpass: 100,
59
+ noiseMix: 0.02,
60
+ },
61
+ coin: {
62
+ waveform: 'square',
63
+ frequency: 880,
64
+ duration: 0.24,
65
+ decay: 0.44,
66
+ sweep: 0.28,
67
+ vibrato: 0.02,
68
+ lowpass: 11000,
69
+ highpass: 140,
70
+ noiseMix: 0,
71
+ },
72
+ powerUp: {
73
+ waveform: 'triangle',
74
+ frequency: 330,
75
+ duration: 0.78,
76
+ decay: 0.24,
77
+ sweep: 0.82,
78
+ vibrato: 0.11,
79
+ lowpass: 10000,
80
+ highpass: 90,
81
+ noiseMix: 0.01,
82
+ },
83
+ };
84
+
85
+ export function clamp(value: number, min: number, max: number): number {
86
+ return Math.min(max, Math.max(min, value));
87
+ }
88
+
89
+ export function createDefaultParams(): SfxParams {
90
+ return getPresetParams('explosion', () => 0.5);
91
+ }
92
+
93
+ export function getPresetParams(preset: SfxPreset, random: () => number = Math.random): SfxParams {
94
+ const base = PRESET_RANGES[preset];
95
+ const frequencyJitter = 1 + (random() - 0.5) * 0.18;
96
+ const durationJitter = 1 + (random() - 0.5) * 0.12;
97
+ const sweepJitter = (random() - 0.5) * 0.16;
98
+ const frequency = clamp(base.frequency * frequencyJitter, 60, 8000);
99
+ const duration = clamp(base.duration * durationJitter, 0.08, 2.5);
100
+ const sweep = clamp(base.sweep + sweepJitter, -1, 1);
101
+
102
+ return {
103
+ ...base,
104
+ frequency,
105
+ frequencyEnd: calculateEndFrequency(frequency, sweep),
106
+ duration,
107
+ sweep,
108
+ };
109
+ }
110
+
111
+ export function calculateEndFrequency(frequency: number, sweep: number): number {
112
+ const normalizedSweep = clamp(sweep, -1, 1);
113
+ const ratio = normalizedSweep >= 0
114
+ ? 1 + normalizedSweep * 11
115
+ : 1 / (1 + Math.abs(normalizedSweep) * 11);
116
+ return clamp(frequency * ratio, 40, 12000);
117
+ }
118
+
119
+ export function updateSweep(params: SfxParams, sweep: number): SfxParams {
120
+ const safeSweep = clamp(sweep, -1, 1);
121
+ return { ...params, sweep: safeSweep, frequencyEnd: calculateEndFrequency(params.frequency, safeSweep) };
122
+ }
123
+
124
+ function seededNoise(index: number): number {
125
+ const value = Math.sin((index + 1) * 12.9898) * 43758.5453;
126
+ return (value - Math.floor(value)) * 2 - 1;
127
+ }
128
+
129
+ function waveformSample(waveform: SfxWaveform, phase: number, index: number): number {
130
+ const cycle = phase - Math.floor(phase);
131
+ if (waveform === 'square') return cycle < 0.5 ? 1 : -1;
132
+ if (waveform === 'sawtooth') return cycle * 2 - 1;
133
+ if (waveform === 'triangle') return 1 - 4 * Math.abs(Math.round(cycle) - cycle);
134
+ if (waveform === 'noise') return seededNoise(index);
135
+ return Math.sin(cycle * Math.PI * 2);
136
+ }
137
+
138
+ function filterAlpha(cutoff: number, sampleRate: number): number {
139
+ const safeCutoff = clamp(cutoff, 1, sampleRate * 0.45);
140
+ return 1 - Math.exp((-2 * Math.PI * safeCutoff) / sampleRate);
141
+ }
142
+
143
+ export function generateSfx(params: SfxParams, sampleRate = DEFAULT_SAMPLE_RATE): GeneratedSfx {
144
+ const safeSampleRate = Math.max(MIN_SAMPLE_RATE, Math.round(sampleRate));
145
+ const duration = clamp(params.duration, 0.04, 4);
146
+ const frameCount = Math.max(1, Math.round(duration * safeSampleRate));
147
+ const samples = new Float32Array(frameCount);
148
+ const lowAlpha = filterAlpha(params.lowpass, safeSampleRate);
149
+ const highAlpha = filterAlpha(params.highpass, safeSampleRate);
150
+ const noiseMix = clamp(params.noiseMix, 0, 1);
151
+ const vibrato = clamp(params.vibrato, 0, 1);
152
+ let phase = 0;
153
+ let lowState = 0;
154
+ let highState = 0;
155
+
156
+ for (let index = 0; index < frameCount; index += 1) {
157
+ const progress = frameCount === 1 ? 1 : index / (frameCount - 1);
158
+ const frequency = Math.max(20, params.frequency + (params.frequencyEnd - params.frequency) * progress);
159
+ const vibratoFactor = 1 + Math.sin(progress * Math.PI * 2 * 7) * vibrato * 0.08;
160
+ phase += (frequency * vibratoFactor) / safeSampleRate;
161
+ const tone = waveformSample(params.waveform, phase, index);
162
+ const noise = seededNoise(index + 1987);
163
+ const mixed = tone * (1 - noiseMix) + noise * noiseMix;
164
+ lowState += lowAlpha * (mixed - lowState);
165
+ highState += highAlpha * (mixed - highState);
166
+ const filtered = lowState * 0.7 + (mixed - highState) * 0.3;
167
+ const attack = Math.min(1, progress / 0.025);
168
+ const decayPower = 0.35 + clamp(params.decay, 0, 1) * 4;
169
+ const envelope = attack * Math.pow(1 - progress, decayPower);
170
+ samples[index] = clamp(filtered * envelope * 0.92, -1, 1);
171
+ }
172
+
173
+ return { samples, sampleRate: safeSampleRate, duration: frameCount / safeSampleRate };
174
+ }
175
+
176
+ export function encodeWav(samples: Float32Array, sampleRate: number): Uint8Array {
177
+ const safeSampleRate = Math.max(MIN_SAMPLE_RATE, Math.round(sampleRate));
178
+ const dataSize = samples.length * 2;
179
+ const buffer = new Uint8Array(44 + dataSize);
180
+ const view = new DataView(buffer.buffer);
181
+ const writeText = (offset: number, value: string): void => {
182
+ for (let index = 0; index < value.length; index += 1) buffer[offset + index] = value.charCodeAt(index);
183
+ };
184
+
185
+ writeText(0, 'RIFF');
186
+ view.setUint32(4, 36 + dataSize, true);
187
+ writeText(8, 'WAVE');
188
+ writeText(12, 'fmt ');
189
+ view.setUint32(16, 16, true);
190
+ view.setUint16(20, 1, true);
191
+ view.setUint16(22, 1, true);
192
+ view.setUint32(24, safeSampleRate, true);
193
+ view.setUint32(28, safeSampleRate * 2, true);
194
+ view.setUint16(32, 2, true);
195
+ view.setUint16(34, 16, true);
196
+ writeText(36, 'data');
197
+ view.setUint32(40, dataSize, true);
198
+
199
+ for (let index = 0; index < samples.length; index += 1) {
200
+ const sample = clamp(samples[index] ?? 0, -1, 1);
201
+ view.setInt16(44 + index * 2, Math.round(sample < 0 ? sample * 0x8000 : sample * 0x7fff), true);
202
+ }
203
+
204
+ return buffer;
205
+ }
206
+
207
+ export function formatFrequency(value: number): string {
208
+ if (value >= 1000) return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)} kHz`;
209
+ return `${Math.round(value)} Hz`;
210
+ }
211
+
212
+ export function formatDuration(value: number): string {
213
+ return `${value.toFixed(2)} s`;
214
+ }
@@ -0,0 +1,435 @@
1
+ .rsfx-app {
2
+ --rsfx-ink: #282747;
3
+ --rsfx-ink-strong: #211d3d;
4
+ --rsfx-muted: #625d78;
5
+ --rsfx-line: #756983;
6
+ --rsfx-panel: #fff7de;
7
+ --rsfx-panel-alt: #f2e4bd;
8
+ --rsfx-page: #fffaf0;
9
+ --rsfx-cyan: #315ccf;
10
+ --rsfx-teal: #087f72;
11
+ --rsfx-orange: #d35332;
12
+ --rsfx-yellow: #f2c14e;
13
+ --rsfx-pink: #b0306a;
14
+ --rsfx-cream: #fff8df;
15
+ --rsfx-on-accent: #fff8df;
16
+ --rsfx-play-bg: #bc4d30;
17
+ --rsfx-download-ink: #211d3d;
18
+ --rsfx-shadow: #33294d;
19
+ --rsfx-scope-panel: #f2c14e;
20
+ --rsfx-scope-screen: #fff3c4;
21
+ --rsfx-scope-ink: #282747;
22
+ --rsfx-scope-muted: #514971;
23
+ --rsfx-scope-accent: #b0306a;
24
+ --rsfx-scope-grid: rgba(49, 92, 207, 0.2);
25
+ --rsfx-waveform: #b0306a;
26
+ --rsfx-waveform-glow: rgba(176, 48, 106, 0.42);
27
+
28
+ background:
29
+ linear-gradient(rgba(49, 92, 207, 0.035) 1px, transparent 1px),
30
+ linear-gradient(90deg, rgba(49, 92, 207, 0.035) 1px, transparent 1px),
31
+ var(--rsfx-page);
32
+ background-size: 1rem 1rem;
33
+ border: 3px solid var(--rsfx-ink-strong);
34
+ box-shadow: 6px 6px 0 var(--rsfx-shadow);
35
+ color: var(--rsfx-ink);
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 1.25rem;
39
+ padding: clamp(1rem, 3vw, 2rem);
40
+ }
41
+
42
+ .theme-dark .rsfx-app {
43
+ --rsfx-ink: #f7edcf;
44
+ --rsfx-ink-strong: #fff5d8;
45
+ --rsfx-muted: #c8b8c4;
46
+ --rsfx-line: #75627b;
47
+ --rsfx-panel: #292443;
48
+ --rsfx-panel-alt: #352d53;
49
+ --rsfx-page: #19162d;
50
+ --rsfx-cyan: #6bd5e4;
51
+ --rsfx-teal: #72f2c8;
52
+ --rsfx-orange: #ff805c;
53
+ --rsfx-yellow: #f2c14e;
54
+ --rsfx-pink: #f477a8;
55
+ --rsfx-cream: #fff8df;
56
+ --rsfx-on-accent: #101b32;
57
+ --rsfx-play-bg: #ff805c;
58
+ --rsfx-download-ink: #101b32;
59
+ --rsfx-shadow: #080714;
60
+ --rsfx-scope-panel: #302555;
61
+ --rsfx-scope-screen: #101b32;
62
+ --rsfx-scope-ink: #f7edcf;
63
+ --rsfx-scope-muted: #b6a8bd;
64
+ --rsfx-scope-accent: #72f2c8;
65
+ --rsfx-scope-grid: rgba(114, 242, 200, 0.16);
66
+ --rsfx-waveform: #72f2c8;
67
+ --rsfx-waveform-glow: rgba(114, 242, 200, 0.75);
68
+
69
+ background:
70
+ linear-gradient(rgba(107, 213, 228, 0.035) 1px, transparent 1px),
71
+ linear-gradient(90deg, rgba(107, 213, 228, 0.035) 1px, transparent 1px),
72
+ var(--rsfx-page);
73
+ }
74
+
75
+ .rsfx-scope-panel {
76
+ background: var(--rsfx-scope-panel);
77
+ border: 3px solid var(--rsfx-ink-strong);
78
+ box-shadow: 4px 4px 0 var(--rsfx-shadow);
79
+ color: var(--rsfx-scope-ink);
80
+ padding: 1rem;
81
+ }
82
+
83
+ .rsfx-scope-topline,
84
+ .rsfx-scope-footer {
85
+ align-items: center;
86
+ display: flex;
87
+ gap: 1rem;
88
+ justify-content: space-between;
89
+ }
90
+
91
+ .rsfx-scope-topline {
92
+ min-height: 2rem;
93
+ }
94
+
95
+ .rsfx-kicker,
96
+ .rsfx-section-label {
97
+ display: block;
98
+ font-size: 0.75rem;
99
+ font-weight: 900;
100
+ letter-spacing: 0.13em;
101
+ text-transform: uppercase;
102
+ }
103
+
104
+ .rsfx-kicker {
105
+ color: var(--rsfx-scope-accent);
106
+ }
107
+
108
+ .rsfx-section-label::before {
109
+ color: var(--rsfx-orange);
110
+ content: '> ';
111
+ }
112
+
113
+ .rsfx-scope-readout {
114
+ color: var(--rsfx-scope-muted);
115
+ font: 0.76rem ui-monospace, monospace;
116
+ margin-left: 0.75rem;
117
+ }
118
+
119
+ .rsfx-scope-led {
120
+ background: var(--rsfx-scope-muted);
121
+ box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.22);
122
+ height: 0.65rem;
123
+ transition: background 0.2s ease, box-shadow 0.2s ease;
124
+ width: 0.65rem;
125
+ }
126
+
127
+ .rsfx-scope-led.is-on {
128
+ background: var(--rsfx-scope-accent);
129
+ box-shadow: 0 0 0.8rem 0.2rem var(--rsfx-waveform-glow);
130
+ }
131
+
132
+ .rsfx-scope-screen {
133
+ background: var(--rsfx-scope-screen);
134
+ border: 2px solid var(--rsfx-scope-accent);
135
+ margin: 0.6rem 0;
136
+ min-height: 13rem;
137
+ overflow: hidden;
138
+ position: relative;
139
+ }
140
+
141
+ .rsfx-waveform,
142
+ .rsfx-scope-grid {
143
+ display: block;
144
+ height: 13rem;
145
+ width: 100%;
146
+ }
147
+
148
+ .rsfx-scope-grid {
149
+ background-image: linear-gradient(var(--rsfx-scope-grid) 1px, transparent 1px), linear-gradient(90deg, var(--rsfx-scope-grid) 1px, transparent 1px);
150
+ background-size: 12.5% 25%;
151
+ pointer-events: none;
152
+ position: absolute;
153
+ inset: 0;
154
+ }
155
+
156
+ .rsfx-scope-footer {
157
+ color: var(--rsfx-scope-muted);
158
+ flex-wrap: wrap;
159
+ font: 0.75rem ui-monospace, monospace;
160
+ }
161
+
162
+ .rsfx-preset-strip,
163
+ .rsfx-control-section {
164
+ background: var(--rsfx-panel);
165
+ border: 2px solid var(--rsfx-ink-strong);
166
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
167
+ padding: 1rem;
168
+ }
169
+
170
+ .rsfx-preset-strip {
171
+ display: flex;
172
+ flex-direction: column;
173
+ gap: 0.75rem;
174
+ }
175
+
176
+ .rsfx-preset-grid {
177
+ display: grid;
178
+ gap: 0.6rem;
179
+ grid-template-columns: repeat(5, minmax(0, 1fr));
180
+ }
181
+
182
+ .rsfx-preset,
183
+ .rsfx-text-action,
184
+ .rsfx-action,
185
+ .rsfx-select {
186
+ font: inherit;
187
+ }
188
+
189
+ .rsfx-preset {
190
+ align-items: center;
191
+ background: var(--rsfx-panel-alt);
192
+ border: 2px solid var(--rsfx-line);
193
+ color: var(--rsfx-ink);
194
+ cursor: pointer;
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 0.25rem;
198
+ justify-content: center;
199
+ min-height: 4.4rem;
200
+ padding: 0.5rem;
201
+ transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
202
+ }
203
+
204
+ .rsfx-preset:hover,
205
+ .rsfx-preset:focus-visible {
206
+ border-color: var(--rsfx-cyan);
207
+ box-shadow: 3px 3px 0 var(--rsfx-cyan);
208
+ transform: translate(-1px, -1px);
209
+ }
210
+
211
+ .rsfx-preset.is-active {
212
+ background: var(--rsfx-cyan);
213
+ border-color: var(--rsfx-ink-strong);
214
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
215
+ color: var(--rsfx-on-accent);
216
+ }
217
+
218
+ .rsfx-preset-glyph {
219
+ color: var(--rsfx-orange);
220
+ font-size: 1.35rem;
221
+ font-weight: 900;
222
+ line-height: 1;
223
+ }
224
+
225
+ .rsfx-preset.is-active .rsfx-preset-glyph {
226
+ color: var(--rsfx-yellow);
227
+ }
228
+
229
+ .rsfx-preset span:last-child {
230
+ font-size: 0.78rem;
231
+ font-weight: 800;
232
+ }
233
+
234
+ .rsfx-hint {
235
+ color: var(--rsfx-muted);
236
+ font-size: 0.8rem;
237
+ margin: 0;
238
+ }
239
+
240
+ .rsfx-control-board {
241
+ display: grid;
242
+ gap: 1rem;
243
+ grid-template-columns: repeat(3, minmax(0, 1fr));
244
+ }
245
+
246
+ .rsfx-control-section {
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: 1rem;
250
+ }
251
+
252
+ .rsfx-waveform-picker {
253
+ display: grid;
254
+ gap: 0.65rem;
255
+ grid-template-columns: 1fr;
256
+ }
257
+
258
+ .rsfx-control-label,
259
+ .rsfx-slider-control > span {
260
+ font-size: 0.8rem;
261
+ font-weight: 800;
262
+ }
263
+
264
+ .rsfx-select {
265
+ appearance: none;
266
+ background: var(--rsfx-panel-alt);
267
+ border: 2px solid var(--rsfx-line);
268
+ color: var(--rsfx-ink);
269
+ cursor: pointer;
270
+ min-height: 2.35rem;
271
+ padding: 0.45rem 0.7rem;
272
+ width: 100%;
273
+ }
274
+
275
+ .rsfx-slider-grid {
276
+ display: grid;
277
+ gap: 1rem;
278
+ }
279
+
280
+ .rsfx-slider-control {
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 0.55rem;
284
+ }
285
+
286
+ .rsfx-slider-control > span {
287
+ align-items: center;
288
+ display: flex;
289
+ gap: 0.75rem;
290
+ justify-content: space-between;
291
+ }
292
+
293
+ .rsfx-slider-control output {
294
+ color: var(--rsfx-cyan);
295
+ font: 900 0.75rem ui-monospace, monospace;
296
+ }
297
+
298
+ .rsfx-slider-control input {
299
+ accent-color: var(--rsfx-orange);
300
+ cursor: pointer;
301
+ width: 100%;
302
+ }
303
+
304
+ .rsfx-slider-control input::-webkit-slider-runnable-track {
305
+ background: var(--rsfx-line);
306
+ border: 1px solid var(--rsfx-ink);
307
+ height: 0.55rem;
308
+ }
309
+
310
+ .rsfx-slider-control input::-webkit-slider-thumb {
311
+ appearance: none;
312
+ background: var(--rsfx-orange);
313
+ border: 2px solid var(--rsfx-ink-strong);
314
+ height: 1.15rem;
315
+ margin-top: -0.35rem;
316
+ width: 0.8rem;
317
+ }
318
+
319
+ .rsfx-slider-control input::-moz-range-track {
320
+ background: var(--rsfx-line);
321
+ border: 1px solid var(--rsfx-ink);
322
+ height: 0.55rem;
323
+ }
324
+
325
+ .rsfx-slider-control input::-moz-range-thumb {
326
+ background: var(--rsfx-orange);
327
+ border: 2px solid var(--rsfx-ink-strong);
328
+ border-radius: 0;
329
+ height: 1.15rem;
330
+ width: 0.8rem;
331
+ }
332
+
333
+ .rsfx-action-bar {
334
+ align-items: center;
335
+ display: flex;
336
+ flex-wrap: wrap;
337
+ gap: 1rem;
338
+ justify-content: space-between;
339
+ }
340
+
341
+ .rsfx-primary-actions,
342
+ .rsfx-secondary-actions {
343
+ display: flex;
344
+ flex-wrap: wrap;
345
+ gap: 0.6rem;
346
+ }
347
+
348
+ .rsfx-action,
349
+ .rsfx-text-action {
350
+ cursor: pointer;
351
+ font-size: 0.82rem;
352
+ font-weight: 900;
353
+ min-height: 2.55rem;
354
+ padding: 0.65rem 1rem;
355
+ transition: transform 0.18s ease, filter 0.18s ease, background 0.18s ease;
356
+ }
357
+
358
+ .rsfx-action:hover,
359
+ .rsfx-text-action:hover {
360
+ filter: brightness(1.08);
361
+ transform: translate(-1px, -1px);
362
+ }
363
+
364
+ .rsfx-action-play {
365
+ background: var(--rsfx-play-bg);
366
+ border: 2px solid var(--rsfx-ink-strong);
367
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
368
+ color: var(--rsfx-on-accent);
369
+ }
370
+
371
+ .rsfx-action-stop {
372
+ background: var(--rsfx-panel);
373
+ border: 2px solid var(--rsfx-ink-strong);
374
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
375
+ color: var(--rsfx-ink);
376
+ }
377
+
378
+ .rsfx-action-download {
379
+ background: var(--rsfx-yellow);
380
+ border: 2px solid var(--rsfx-ink-strong);
381
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
382
+ color: var(--rsfx-download-ink);
383
+ }
384
+
385
+ .rsfx-text-action {
386
+ background: transparent;
387
+ border: 2px solid transparent;
388
+ color: var(--rsfx-cyan);
389
+ }
390
+
391
+ .rsfx-status {
392
+ border-top: 2px solid var(--rsfx-line);
393
+ color: var(--rsfx-muted);
394
+ font: 0.75rem ui-monospace, monospace;
395
+ margin: 0;
396
+ padding-top: 0.85rem;
397
+ }
398
+
399
+ .rsfx-app button:focus-visible,
400
+ .rsfx-app select:focus-visible,
401
+ .rsfx-app input:focus-visible {
402
+ outline: 3px solid var(--rsfx-yellow);
403
+ outline-offset: 2px;
404
+ }
405
+
406
+ @media (max-width: 820px) {
407
+ .rsfx-control-board {
408
+ grid-template-columns: 1fr;
409
+ }
410
+ }
411
+
412
+ @media (max-width: 560px) {
413
+ .rsfx-preset-grid {
414
+ grid-template-columns: repeat(2, minmax(0, 1fr));
415
+ }
416
+
417
+ .rsfx-preset:last-child {
418
+ grid-column: span 2;
419
+ }
420
+
421
+ .rsfx-scope-footer span:last-child {
422
+ width: 100%;
423
+ }
424
+
425
+ .rsfx-action-bar,
426
+ .rsfx-primary-actions,
427
+ .rsfx-secondary-actions {
428
+ width: 100%;
429
+ }
430
+
431
+ .rsfx-action,
432
+ .rsfx-text-action {
433
+ flex: 1;
434
+ }
435
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { retroSfxGenerator } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = retroSfxGenerator.i18n[locale] ?? retroSfxGenerator.i18n.en;
12
+ const content = loader ? await loader() : null;
13
+ ---
14
+
15
+ {content && content.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}