@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
@@ -0,0 +1,429 @@
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-shadow: #33294d;
16
+ --rsfx-scope-panel: #f2c14e;
17
+ --rsfx-scope-screen: #fff3c4;
18
+ --rsfx-scope-ink: #282747;
19
+ --rsfx-scope-muted: #514971;
20
+ --rsfx-scope-accent: #b0306a;
21
+ --rsfx-scope-grid: rgba(49, 92, 207, 0.2);
22
+ --rsfx-waveform: #b0306a;
23
+ --rsfx-waveform-glow: rgba(176, 48, 106, 0.42);
24
+
25
+ background:
26
+ linear-gradient(rgba(49, 92, 207, 0.035) 1px, transparent 1px),
27
+ linear-gradient(90deg, rgba(49, 92, 207, 0.035) 1px, transparent 1px),
28
+ var(--rsfx-page);
29
+ background-size: 1rem 1rem;
30
+ border: 3px solid var(--rsfx-ink-strong);
31
+ box-shadow: 6px 6px 0 var(--rsfx-shadow);
32
+ color: var(--rsfx-ink);
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 1.25rem;
36
+ padding: clamp(1rem, 3vw, 2rem);
37
+ }
38
+
39
+ .theme-dark .rsfx-app {
40
+ --rsfx-ink: #f7edcf;
41
+ --rsfx-ink-strong: #fff5d8;
42
+ --rsfx-muted: #c8b8c4;
43
+ --rsfx-line: #75627b;
44
+ --rsfx-panel: #292443;
45
+ --rsfx-panel-alt: #352d53;
46
+ --rsfx-page: #19162d;
47
+ --rsfx-cyan: #6bd5e4;
48
+ --rsfx-teal: #72f2c8;
49
+ --rsfx-orange: #ff805c;
50
+ --rsfx-yellow: #f2c14e;
51
+ --rsfx-pink: #f477a8;
52
+ --rsfx-cream: #fff8df;
53
+ --rsfx-shadow: #080714;
54
+ --rsfx-scope-panel: #302555;
55
+ --rsfx-scope-screen: #101b32;
56
+ --rsfx-scope-ink: #f7edcf;
57
+ --rsfx-scope-muted: #b6a8bd;
58
+ --rsfx-scope-accent: #72f2c8;
59
+ --rsfx-scope-grid: rgba(114, 242, 200, 0.16);
60
+ --rsfx-waveform: #72f2c8;
61
+ --rsfx-waveform-glow: rgba(114, 242, 200, 0.75);
62
+
63
+ background:
64
+ linear-gradient(rgba(107, 213, 228, 0.035) 1px, transparent 1px),
65
+ linear-gradient(90deg, rgba(107, 213, 228, 0.035) 1px, transparent 1px),
66
+ var(--rsfx-page);
67
+ }
68
+
69
+ .rsfx-scope-panel {
70
+ background: var(--rsfx-scope-panel);
71
+ border: 3px solid var(--rsfx-ink-strong);
72
+ box-shadow: 4px 4px 0 var(--rsfx-shadow);
73
+ color: var(--rsfx-scope-ink);
74
+ padding: 1rem;
75
+ }
76
+
77
+ .rsfx-scope-topline,
78
+ .rsfx-scope-footer {
79
+ align-items: center;
80
+ display: flex;
81
+ gap: 1rem;
82
+ justify-content: space-between;
83
+ }
84
+
85
+ .rsfx-scope-topline {
86
+ min-height: 2rem;
87
+ }
88
+
89
+ .rsfx-kicker,
90
+ .rsfx-section-label {
91
+ display: block;
92
+ font-size: 0.75rem;
93
+ font-weight: 900;
94
+ letter-spacing: 0.13em;
95
+ text-transform: uppercase;
96
+ }
97
+
98
+ .rsfx-kicker {
99
+ color: var(--rsfx-scope-accent);
100
+ }
101
+
102
+ .rsfx-section-label::before {
103
+ color: var(--rsfx-orange);
104
+ content: '> ';
105
+ }
106
+
107
+ .rsfx-scope-readout {
108
+ color: var(--rsfx-scope-muted);
109
+ font: 0.76rem ui-monospace, monospace;
110
+ margin-left: 0.75rem;
111
+ }
112
+
113
+ .rsfx-scope-led {
114
+ background: var(--rsfx-scope-muted);
115
+ box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.22);
116
+ height: 0.65rem;
117
+ transition: background 0.2s ease, box-shadow 0.2s ease;
118
+ width: 0.65rem;
119
+ }
120
+
121
+ .rsfx-scope-led.is-on {
122
+ background: var(--rsfx-scope-accent);
123
+ box-shadow: 0 0 0.8rem 0.2rem var(--rsfx-waveform-glow);
124
+ }
125
+
126
+ .rsfx-scope-screen {
127
+ background: var(--rsfx-scope-screen);
128
+ border: 2px solid var(--rsfx-scope-accent);
129
+ margin: 0.6rem 0;
130
+ min-height: 13rem;
131
+ overflow: hidden;
132
+ position: relative;
133
+ }
134
+
135
+ .rsfx-waveform,
136
+ .rsfx-scope-grid {
137
+ display: block;
138
+ height: 13rem;
139
+ width: 100%;
140
+ }
141
+
142
+ .rsfx-scope-grid {
143
+ background-image: linear-gradient(var(--rsfx-scope-grid) 1px, transparent 1px), linear-gradient(90deg, var(--rsfx-scope-grid) 1px, transparent 1px);
144
+ background-size: 12.5% 25%;
145
+ pointer-events: none;
146
+ position: absolute;
147
+ inset: 0;
148
+ }
149
+
150
+ .rsfx-scope-footer {
151
+ color: var(--rsfx-scope-muted);
152
+ flex-wrap: wrap;
153
+ font: 0.75rem ui-monospace, monospace;
154
+ }
155
+
156
+ .rsfx-preset-strip,
157
+ .rsfx-control-section {
158
+ background: var(--rsfx-panel);
159
+ border: 2px solid var(--rsfx-ink-strong);
160
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
161
+ padding: 1rem;
162
+ }
163
+
164
+ .rsfx-preset-strip {
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 0.75rem;
168
+ }
169
+
170
+ .rsfx-preset-grid {
171
+ display: grid;
172
+ gap: 0.6rem;
173
+ grid-template-columns: repeat(5, minmax(0, 1fr));
174
+ }
175
+
176
+ .rsfx-preset,
177
+ .rsfx-text-action,
178
+ .rsfx-action,
179
+ .rsfx-select {
180
+ font: inherit;
181
+ }
182
+
183
+ .rsfx-preset {
184
+ align-items: center;
185
+ background: var(--rsfx-panel-alt);
186
+ border: 2px solid var(--rsfx-line);
187
+ color: var(--rsfx-ink);
188
+ cursor: pointer;
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 0.25rem;
192
+ justify-content: center;
193
+ min-height: 4.4rem;
194
+ padding: 0.5rem;
195
+ transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
196
+ }
197
+
198
+ .rsfx-preset:hover,
199
+ .rsfx-preset:focus-visible {
200
+ border-color: var(--rsfx-cyan);
201
+ box-shadow: 3px 3px 0 var(--rsfx-cyan);
202
+ transform: translate(-1px, -1px);
203
+ }
204
+
205
+ .rsfx-preset.is-active {
206
+ background: var(--rsfx-cyan);
207
+ border-color: var(--rsfx-ink-strong);
208
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
209
+ color: var(--rsfx-cream);
210
+ }
211
+
212
+ .rsfx-preset-glyph {
213
+ color: var(--rsfx-orange);
214
+ font-size: 1.35rem;
215
+ font-weight: 900;
216
+ line-height: 1;
217
+ }
218
+
219
+ .rsfx-preset.is-active .rsfx-preset-glyph {
220
+ color: var(--rsfx-yellow);
221
+ }
222
+
223
+ .rsfx-preset span:last-child {
224
+ font-size: 0.78rem;
225
+ font-weight: 800;
226
+ }
227
+
228
+ .rsfx-hint {
229
+ color: var(--rsfx-muted);
230
+ font-size: 0.8rem;
231
+ margin: 0;
232
+ }
233
+
234
+ .rsfx-control-board {
235
+ display: grid;
236
+ gap: 1rem;
237
+ grid-template-columns: repeat(3, minmax(0, 1fr));
238
+ }
239
+
240
+ .rsfx-control-section {
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 1rem;
244
+ }
245
+
246
+ .rsfx-waveform-picker {
247
+ display: grid;
248
+ gap: 0.65rem;
249
+ grid-template-columns: 1fr;
250
+ }
251
+
252
+ .rsfx-control-label,
253
+ .rsfx-slider-control > span {
254
+ font-size: 0.8rem;
255
+ font-weight: 800;
256
+ }
257
+
258
+ .rsfx-select {
259
+ appearance: none;
260
+ background: var(--rsfx-panel-alt);
261
+ border: 2px solid var(--rsfx-line);
262
+ color: var(--rsfx-ink);
263
+ cursor: pointer;
264
+ min-height: 2.35rem;
265
+ padding: 0.45rem 0.7rem;
266
+ width: 100%;
267
+ }
268
+
269
+ .rsfx-slider-grid {
270
+ display: grid;
271
+ gap: 1rem;
272
+ }
273
+
274
+ .rsfx-slider-control {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 0.55rem;
278
+ }
279
+
280
+ .rsfx-slider-control > span {
281
+ align-items: center;
282
+ display: flex;
283
+ gap: 0.75rem;
284
+ justify-content: space-between;
285
+ }
286
+
287
+ .rsfx-slider-control output {
288
+ color: var(--rsfx-cyan);
289
+ font: 900 0.75rem ui-monospace, monospace;
290
+ }
291
+
292
+ .rsfx-slider-control input {
293
+ accent-color: var(--rsfx-orange);
294
+ cursor: pointer;
295
+ width: 100%;
296
+ }
297
+
298
+ .rsfx-slider-control input::-webkit-slider-runnable-track {
299
+ background: var(--rsfx-line);
300
+ border: 1px solid var(--rsfx-ink);
301
+ height: 0.55rem;
302
+ }
303
+
304
+ .rsfx-slider-control input::-webkit-slider-thumb {
305
+ appearance: none;
306
+ background: var(--rsfx-orange);
307
+ border: 2px solid var(--rsfx-ink-strong);
308
+ height: 1.15rem;
309
+ margin-top: -0.35rem;
310
+ width: 0.8rem;
311
+ }
312
+
313
+ .rsfx-slider-control input::-moz-range-track {
314
+ background: var(--rsfx-line);
315
+ border: 1px solid var(--rsfx-ink);
316
+ height: 0.55rem;
317
+ }
318
+
319
+ .rsfx-slider-control input::-moz-range-thumb {
320
+ background: var(--rsfx-orange);
321
+ border: 2px solid var(--rsfx-ink-strong);
322
+ border-radius: 0;
323
+ height: 1.15rem;
324
+ width: 0.8rem;
325
+ }
326
+
327
+ .rsfx-action-bar {
328
+ align-items: center;
329
+ display: flex;
330
+ flex-wrap: wrap;
331
+ gap: 1rem;
332
+ justify-content: space-between;
333
+ }
334
+
335
+ .rsfx-primary-actions,
336
+ .rsfx-secondary-actions {
337
+ display: flex;
338
+ flex-wrap: wrap;
339
+ gap: 0.6rem;
340
+ }
341
+
342
+ .rsfx-action,
343
+ .rsfx-text-action {
344
+ cursor: pointer;
345
+ font-size: 0.82rem;
346
+ font-weight: 900;
347
+ min-height: 2.55rem;
348
+ padding: 0.65rem 1rem;
349
+ transition: transform 0.18s ease, filter 0.18s ease, background 0.18s ease;
350
+ }
351
+
352
+ .rsfx-action:hover,
353
+ .rsfx-text-action:hover {
354
+ filter: brightness(1.08);
355
+ transform: translate(-1px, -1px);
356
+ }
357
+
358
+ .rsfx-action-play {
359
+ background: var(--rsfx-orange);
360
+ border: 2px solid var(--rsfx-ink-strong);
361
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
362
+ color: var(--rsfx-cream);
363
+ }
364
+
365
+ .rsfx-action-stop {
366
+ background: var(--rsfx-panel);
367
+ border: 2px solid var(--rsfx-ink-strong);
368
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
369
+ color: var(--rsfx-ink);
370
+ }
371
+
372
+ .rsfx-action-download {
373
+ background: var(--rsfx-yellow);
374
+ border: 2px solid var(--rsfx-ink-strong);
375
+ box-shadow: 3px 3px 0 var(--rsfx-shadow);
376
+ color: var(--rsfx-ink-strong);
377
+ }
378
+
379
+ .rsfx-text-action {
380
+ background: transparent;
381
+ border: 2px solid transparent;
382
+ color: var(--rsfx-cyan);
383
+ }
384
+
385
+ .rsfx-status {
386
+ border-top: 2px solid var(--rsfx-line);
387
+ color: var(--rsfx-muted);
388
+ font: 0.75rem ui-monospace, monospace;
389
+ margin: 0;
390
+ padding-top: 0.85rem;
391
+ }
392
+
393
+ .rsfx-app button:focus-visible,
394
+ .rsfx-app select:focus-visible,
395
+ .rsfx-app input:focus-visible {
396
+ outline: 3px solid var(--rsfx-yellow);
397
+ outline-offset: 2px;
398
+ }
399
+
400
+ @media (max-width: 820px) {
401
+ .rsfx-control-board {
402
+ grid-template-columns: 1fr;
403
+ }
404
+ }
405
+
406
+ @media (max-width: 560px) {
407
+ .rsfx-preset-grid {
408
+ grid-template-columns: repeat(2, minmax(0, 1fr));
409
+ }
410
+
411
+ .rsfx-preset:last-child {
412
+ grid-column: span 2;
413
+ }
414
+
415
+ .rsfx-scope-footer span:last-child {
416
+ width: 100%;
417
+ }
418
+
419
+ .rsfx-action-bar,
420
+ .rsfx-primary-actions,
421
+ .rsfx-secondary-actions {
422
+ width: 100%;
423
+ }
424
+
425
+ .rsfx-action,
426
+ .rsfx-text-action {
427
+ flex: 1;
428
+ }
429
+ }
@@ -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 }} />}
@@ -0,0 +1,41 @@
1
+ export interface RetroSfxGeneratorUI extends Record<string, string> {
2
+ waveformLabel: string;
3
+ waveformSquare: string;
4
+ waveformSawtooth: string;
5
+ waveformSine: string;
6
+ waveformTriangle: string;
7
+ waveformNoise: string;
8
+ presetLabel: string;
9
+ presetExplosion: string;
10
+ presetLaser: string;
11
+ presetJump: string;
12
+ presetCoin: string;
13
+ presetPowerUp: string;
14
+ frequencyLabel: string;
15
+ frequencyEndLabel: string;
16
+ durationLabel: string;
17
+ decayLabel: string;
18
+ sweepLabel: string;
19
+ vibratoLabel: string;
20
+ lowpassLabel: string;
21
+ highpassLabel: string;
22
+ noiseMixLabel: string;
23
+ toneSection: string;
24
+ dynamicsSection: string;
25
+ filterSection: string;
26
+ playButton: string;
27
+ stopButton: string;
28
+ downloadButton: string;
29
+ randomizeButton: string;
30
+ resetButton: string;
31
+ waveformPreviewLabel: string;
32
+ generatedLabel: string;
33
+ statusReady: string;
34
+ statusPlaying: string;
35
+ statusStopped: string;
36
+ statusDownloaded: string;
37
+ statusAudioBlocked: string;
38
+ statusGenerating: string;
39
+ presetHint: string;
40
+ monoWavHint: string;
41
+ }
@@ -138,14 +138,14 @@
138
138
  }
139
139
 
140
140
  .chip-btn.active {
141
- background: var(--accent-base, #3b82f6);
141
+ background: var(--accent-base, #2563eb);
142
142
  color: var(--text-inverse, #fff);
143
- box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3);
143
+ box-shadow: 0 2px 6px rgba(37, 99, 235, 0.3);
144
144
  }
145
145
 
146
146
  .chip-btn:hover:not(.active) {
147
- background: rgba(59, 130, 246, 0.08);
148
- color: var(--accent-base, #3b82f6);
147
+ background: rgba(37, 99, 235, 0.08);
148
+ color: var(--accent-base, #2563eb);
149
149
  }
150
150
 
151
151
  .textarea-code {
@@ -157,7 +157,7 @@
157
157
  padding: 0.75rem 1.25rem;
158
158
  border-radius: 0.5rem;
159
159
  border: none;
160
- background: var(--accent-base, #3b82f6);
160
+ background: var(--accent-base, #2563eb);
161
161
  color: var(--text-inverse, #fff);
162
162
  font-weight: 700;
163
163
  cursor: pointer;
@@ -56,7 +56,7 @@
56
56
 
57
57
  .ssp-tab-btn.active {
58
58
  background: var(--bg-surface, #fff);
59
- color: var(--color-primary, #6366f1);
59
+ color: var(--color-primary, #4f46e5);
60
60
  opacity: 1;
61
61
  border: 1px solid var(--border-base, #cbd5e1);
62
62
  border-bottom: 1px solid var(--bg-surface, #fff);
@@ -309,14 +309,14 @@
309
309
  }
310
310
 
311
311
  .ssp-btn-danger {
312
- background: #ef4444;
312
+ background: #dc2626;
313
313
  color: var(--text-on-primary, #fff);
314
314
  }
315
315
 
316
316
  .ssp-btn-success {
317
- background: #10b981;
317
+ background: #047857;
318
318
  color: var(--text-on-primary, #fff);
319
- border-color: #10b981;
319
+ border-color: #047857;
320
320
  }
321
321
 
322
322
  .ssp-stats-grid {
@@ -512,7 +512,7 @@
512
512
  position: fixed;
513
513
  bottom: 2rem;
514
514
  right: 2rem;
515
- background: #10b981;
515
+ background: #047857;
516
516
  color: var(--text-on-primary, #fff);
517
517
  padding: 0.75rem 1.25rem;
518
518
  border-radius: 8px;
@@ -0,0 +1,125 @@
1
+ import type { AutoTranslationResult, MarkupSyntax, TranslationResult } from './types';
2
+ import { clearSourceFromStorage, loadSourceFromStorage, saveSourceToStorage } from './storage';
3
+ import { detectSyntax, getAutoPreviewHtml, translateToAll } from './logic';
4
+
5
+ interface AppElements {
6
+ root: HTMLElement;
7
+ source: HTMLTextAreaElement;
8
+ detected: HTMLElement;
9
+ outputGrid: HTMLElement;
10
+ preview: HTMLElement;
11
+ characterCount: HTMLElement;
12
+ blockCount: HTMLElement;
13
+ }
14
+
15
+ function getElements(root: HTMLElement): AppElements | null {
16
+ const source = root.querySelector('#bbcode-source') as HTMLTextAreaElement | null;
17
+ const detected = root.querySelector('#bbcode-detected') as HTMLElement | null;
18
+ const outputGrid = root.querySelector('#bbcode-output-grid') as HTMLElement | null;
19
+ const preview = root.querySelector('#bbcode-preview') as HTMLElement | null;
20
+ const characterCount = root.querySelector('#bbcode-character-count') as HTMLElement | null;
21
+ const blockCount = root.querySelector('#bbcode-block-count') as HTMLElement | null;
22
+
23
+ if (!source || !detected || !outputGrid || !preview || !characterCount || !blockCount) {
24
+ return null;
25
+ }
26
+
27
+ return { root, source, detected, outputGrid, preview, characterCount, blockCount };
28
+ }
29
+
30
+ async function copyText(value: string, button: HTMLButtonElement, copiedText: string): Promise<void> {
31
+ await navigator.clipboard?.writeText(value);
32
+ const original = button.textContent;
33
+ button.textContent = copiedText;
34
+ window.setTimeout(() => {
35
+ button.textContent = original;
36
+ }, 1200);
37
+ }
38
+
39
+ function createCard(target: MarkupSyntax, translation: TranslationResult, names: Record<MarkupSyntax, string>, dataset: DOMStringMap): HTMLElement {
40
+ const card = document.createElement('article');
41
+ card.className = 'bbcode-output-card';
42
+
43
+ const heading = document.createElement('div');
44
+ heading.className = 'bbcode-output-card-head';
45
+
46
+ const label = document.createElement('h3');
47
+ label.textContent = names[target];
48
+
49
+ const actions = document.createElement('div');
50
+ actions.className = 'bbcode-output-actions';
51
+
52
+ const copyButton = document.createElement('button');
53
+ copyButton.type = 'button';
54
+ copyButton.className = 'bbcode-icon-button';
55
+ copyButton.textContent = dataset.copy ?? 'Copy';
56
+ copyButton.addEventListener('click', () => copyText(translation.output, copyButton, dataset.copied ?? 'Copied'));
57
+
58
+ actions.append(copyButton);
59
+ heading.append(label, actions);
60
+
61
+ const pre = document.createElement('pre');
62
+ pre.textContent = translation.output;
63
+ card.append(heading, pre);
64
+
65
+ return card;
66
+ }
67
+
68
+ function updateMeta(elements: AppElements, names: Record<MarkupSyntax, string>, result: AutoTranslationResult): void {
69
+ const { root, source, detected, characterCount, blockCount } = elements;
70
+ const syntax = detectSyntax(source.value);
71
+ detected.textContent = source.value.trim()
72
+ ? `${root.dataset.detectedLabel ?? 'Detected'}: ${names[syntax]}`
73
+ : root.dataset.detectedEmpty ?? '';
74
+
75
+ characterCount.textContent = String(source.value.length);
76
+ const firstOutputKey = Object.keys(result.outputs)[0] as MarkupSyntax | undefined;
77
+ const firstBlockCount = firstOutputKey ? result.outputs[firstOutputKey]?.stats.blocks ?? 0 : 0;
78
+ blockCount.textContent = String(firstBlockCount);
79
+ }
80
+
81
+ function renderOutputCards(elements: AppElements, names: Record<MarkupSyntax, string>, result: AutoTranslationResult): void {
82
+ elements.outputGrid.innerHTML = '';
83
+ Object.entries(result.outputs).forEach(([target, translation]) => {
84
+ if (!translation) return;
85
+ const card = createCard(target as MarkupSyntax, translation, names, elements.root.dataset);
86
+ elements.outputGrid.append(card);
87
+ });
88
+ }
89
+
90
+ function renderApp(elements: AppElements, names: Record<MarkupSyntax, string>): void {
91
+ const result = translateToAll(elements.source.value);
92
+ updateMeta(elements, names, result);
93
+ renderOutputCards(elements, names, result);
94
+ elements.preview.innerHTML = getAutoPreviewHtml(elements.source.value) || `<p>${elements.root.dataset.previewEmpty ?? ''}</p>`;
95
+ }
96
+
97
+ export function initSteamBbcodeTranslatorApp(): void {
98
+ const root = document.getElementById('steam-bbcode-translator');
99
+ if (!root) return;
100
+ const elements = getElements(root);
101
+ if (!elements) return;
102
+
103
+ const names: Record<MarkupSyntax, string> = {
104
+ bbcode: root.dataset.bbcode ?? 'Steam BBCode',
105
+ markdown: root.dataset.markdown ?? 'Markdown',
106
+ html: root.dataset.html ?? 'HTML'
107
+ };
108
+
109
+ const stored = loadSourceFromStorage();
110
+ if (stored) elements.source.value = stored;
111
+
112
+ elements.source.addEventListener('input', () => {
113
+ saveSourceToStorage(elements.source.value);
114
+ renderApp(elements, names);
115
+ });
116
+
117
+ root.querySelector('#bbcode-clear')?.addEventListener('click', () => {
118
+ elements.source.value = '';
119
+ clearSourceFromStorage();
120
+ renderApp(elements, names);
121
+ elements.source.focus();
122
+ });
123
+
124
+ renderApp(elements, names);
125
+ }