diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs
index dd561d30..f31ed271 100644
--- a/packages/codemirror/themes.mjs
+++ b/packages/codemirror/themes.mjs
@@ -1,4 +1,27 @@
import strudelTheme, { settings as strudelThemeSettings } from './themes/strudel-theme.mjs';
+import neonMadrid, { settings as neonMadridSettings } from './themes/neon-madrid.mjs';
+import {
+ neonCian,
+ neonCianSettings,
+ neonMagenta,
+ neonMagentaSettings,
+ neonVerde,
+ neonVerdeSettings,
+ neonVioleta,
+ neonVioletaSettings,
+ neonRave,
+ neonRaveSettings,
+ madridClaro,
+ madridClaroSettings,
+ madridOscuro,
+ madridOscuroSettings,
+ papelSepia,
+ papelSepiaSettings,
+ medianoche,
+ medianocheSettings,
+ grafito,
+ grafitoSettings,
+} from './themes/hacklab-pack.mjs';
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen.mjs';
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen.mjs';
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
@@ -43,6 +66,17 @@ import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLil
import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
+ neonMadrid,
+ neonCian,
+ neonMagenta,
+ neonVerde,
+ neonVioleta,
+ neonRave,
+ madridClaro,
+ madridOscuro,
+ papelSepia,
+ medianoche,
+ grafito,
algoboy,
archBtw,
androidstudio,
@@ -85,6 +119,17 @@ export const themes = {
export const settings = {
strudelTheme: strudelThemeSettings,
+ neonMadrid: neonMadridSettings,
+ neonCian: neonCianSettings,
+ neonMagenta: neonMagentaSettings,
+ neonVerde: neonVerdeSettings,
+ neonVioleta: neonVioletaSettings,
+ neonRave: neonRaveSettings,
+ madridClaro: madridClaroSettings,
+ madridOscuro: madridOscuroSettings,
+ papelSepia: papelSepiaSettings,
+ medianoche: medianocheSettings,
+ grafito: grafitoSettings,
bluescreen: bluescreenSettings,
bluescreenlight: bluescreenlightsettings,
blackscreen: blackscreenSettings,
diff --git a/packages/codemirror/themes/hacklab-pack.mjs b/packages/codemirror/themes/hacklab-pack.mjs
new file mode 100644
index 00000000..da641d61
--- /dev/null
+++ b/packages/codemirror/themes/hacklab-pack.mjs
@@ -0,0 +1,240 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+// ═══════════════════════════════════════════════════════════════════
+// PACK HACKLAB · 10 temas: 5 NEÓN sobre negro + 5 estándar
+// Mismo formato que neon-madrid.mjs: default = createTheme, settings aparte.
+// ═══════════════════════════════════════════════════════════════════
+
+// ── helper: genera un tema neón sobre negro a partir de 3 acentos ──
+const neon = ({ base, acento, extra, seleccion }) => ({
+ settings: {
+ background: '#050507',
+ lineBackground: '#05050799',
+ foreground: base,
+ muted: `${base}40`,
+ caret: acento,
+ selection: seleccion,
+ selectionMatch: `${acento}33`,
+ lineHighlight: `${base}0d`,
+ gutterBackground: 'transparent',
+ gutterForeground: `${base}66`,
+ },
+ theme: createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#050507',
+ lineBackground: '#05050799',
+ foreground: base,
+ caret: acento,
+ selection: seleccion,
+ selectionMatch: `${acento}33`,
+ lineHighlight: `${base}0d`,
+ gutterBackground: 'transparent',
+ gutterForeground: `${base}66`,
+ },
+ styles: [
+ { tag: [t.atom, t.bool, t.number], color: extra },
+ { tag: t.labelName, color: acento },
+ { tag: t.keyword, color: acento },
+ { tag: t.operator, color: acento },
+ { tag: [t.variableName, t.propertyName, t.tagName, t.typeName], color: base },
+ { tag: t.definition(t.variableName), color: extra },
+ { tag: [t.string, t.special(t.string)], color: extra },
+ { tag: t.comment, color: `${base}77` },
+ { tag: [t.bracket, t.unit, t.punctuation], color: acento },
+ { tag: [t.meta, t.attributeName], color: extra },
+ { tag: t.className, color: extra },
+ { tag: t.invalid, color: '#ff2222' },
+ ],
+ }),
+});
+
+// ── 5 NEÓN ──────────────────────────────────────────────────────────
+
+// cian eléctrico + magenta
+const neonCian_ = neon({ base: '#00e5ff', acento: '#ff00d4', extra: '#7dff00', seleccion: 'rgba(255,0,212,.25)' });
+export const neonCianSettings = neonCian_.settings;
+export const neonCian = neonCian_.theme;
+
+// magenta/rosa + cian
+const neonMagenta_ = neon({ base: '#ff2ec4', acento: '#00e5ff', extra: '#ffe600', seleccion: 'rgba(0,229,255,.25)' });
+export const neonMagentaSettings = neonMagenta_.settings;
+export const neonMagenta = neonMagenta_.theme;
+
+// verde matrix + lima
+const neonVerde_ = neon({ base: '#39ff14', acento: '#00ffc8', extra: '#eaff00', seleccion: 'rgba(0,255,200,.25)' });
+export const neonVerdeSettings = neonVerde_.settings;
+export const neonVerde = neonVerde_.theme;
+
+// violeta + rosa
+const neonVioleta_ = neon({ base: '#b26bff', acento: '#ff4fd8', extra: '#00e5ff', seleccion: 'rgba(255,79,216,.25)' });
+export const neonVioletaSettings = neonVioleta_.settings;
+export const neonVioleta = neonVioleta_.theme;
+
+// rave multicolor: base ámbar, acentos cian/verde (fiesta total)
+const neonRave_ = neon({ base: '#ffb300', acento: '#00e5ff', extra: '#ff2ec4', seleccion: 'rgba(57,255,20,.25)' });
+export const neonRaveSettings = neonRave_.settings;
+export const neonRave = neonRave_.theme;
+
+// ── 5 ESTÁNDAR ──────────────────────────────────────────────────────
+
+// claro limpio (blanco, azules/teal — para trabajar de día)
+export const madridClaroSettings = {
+ background: '#ffffff',
+ lineBackground: '#ffffff99',
+ foreground: '#1e293b',
+ muted: '#1e293b26',
+ caret: '#0369a1',
+ selection: 'rgba(2,132,199,.18)',
+ selectionMatch: 'rgba(2,132,199,.12)',
+ lineHighlight: 'rgba(2,132,199,.05)',
+ gutterBackground: 'transparent',
+ gutterForeground: '#1e293b55',
+};
+export const madridClaro = createTheme({
+ theme: 'light',
+ settings: madridClaroSettings,
+ styles: [
+ { tag: [t.atom, t.bool, t.number], color: '#0d9488' },
+ { tag: t.labelName, color: '#c026d3' },
+ { tag: t.keyword, color: '#7c3aed' },
+ { tag: t.operator, color: '#0369a1' },
+ { tag: [t.variableName, t.propertyName, t.tagName, t.typeName], color: '#0f172a' },
+ { tag: t.definition(t.variableName), color: '#b45309' },
+ { tag: [t.string, t.special(t.string)], color: '#15803d' },
+ { tag: t.comment, color: '#94a3b8' },
+ { tag: [t.bracket, t.unit, t.punctuation], color: '#0369a1' },
+ { tag: [t.meta, t.attributeName], color: '#0d9488' },
+ { tag: t.className, color: '#b45309' },
+ { tag: t.invalid, color: '#dc2626' },
+ ],
+});
+
+// oscuro neutro (gris carbón, acentos suaves — sobrio)
+export const madridOscuroSettings = {
+ background: '#1c1e24',
+ lineBackground: '#1c1e2499',
+ foreground: '#d7dae0',
+ muted: '#d7dae026',
+ caret: '#61afef',
+ selection: 'rgba(97,175,239,.22)',
+ selectionMatch: 'rgba(97,175,239,.15)',
+ lineHighlight: 'rgba(215,218,224,.05)',
+ gutterBackground: 'transparent',
+ gutterForeground: '#d7dae055',
+};
+export const madridOscuro = createTheme({
+ theme: 'dark',
+ settings: madridOscuroSettings,
+ styles: [
+ { tag: [t.atom, t.bool, t.number], color: '#d19a66' },
+ { tag: t.labelName, color: '#c678dd' },
+ { tag: t.keyword, color: '#c678dd' },
+ { tag: t.operator, color: '#56b6c2' },
+ { tag: [t.variableName, t.propertyName, t.tagName, t.typeName], color: '#e5c07b' },
+ { tag: t.definition(t.variableName), color: '#61afef' },
+ { tag: [t.string, t.special(t.string)], color: '#98c379' },
+ { tag: t.comment, color: '#7f848e' },
+ { tag: [t.bracket, t.unit, t.punctuation], color: '#56b6c2' },
+ { tag: [t.meta, t.attributeName], color: '#d19a66' },
+ { tag: t.className, color: '#e5c07b' },
+ { tag: t.invalid, color: '#f14c4c' },
+ ],
+});
+
+// papel sepia (cálido tipo solarized claro — descansa la vista)
+export const papelSepiaSettings = {
+ background: '#f5edda',
+ lineBackground: '#f5edda99',
+ foreground: '#59493a',
+ muted: '#59493a26',
+ caret: '#b58900',
+ selection: 'rgba(181,137,0,.22)',
+ selectionMatch: 'rgba(181,137,0,.14)',
+ lineHighlight: 'rgba(89,73,58,.06)',
+ gutterBackground: 'transparent',
+ gutterForeground: '#59493a55',
+};
+export const papelSepia = createTheme({
+ theme: 'light',
+ settings: papelSepiaSettings,
+ styles: [
+ { tag: [t.atom, t.bool, t.number], color: '#b58900' },
+ { tag: t.labelName, color: '#d33682' },
+ { tag: t.keyword, color: '#859900' },
+ { tag: t.operator, color: '#cb4b16' },
+ { tag: [t.variableName, t.propertyName, t.tagName, t.typeName], color: '#268bd2' },
+ { tag: t.definition(t.variableName), color: '#6c71c4' },
+ { tag: [t.string, t.special(t.string)], color: '#2aa198' },
+ { tag: t.comment, color: '#9d8f77' },
+ { tag: [t.bracket, t.unit, t.punctuation], color: '#cb4b16' },
+ { tag: [t.meta, t.attributeName], color: '#b58900' },
+ { tag: t.className, color: '#6c71c4' },
+ { tag: t.invalid, color: '#dc322f' },
+ ],
+});
+
+// medianoche (azul profundo — clásico nocturno)
+export const medianocheSettings = {
+ background: '#0f172a',
+ lineBackground: '#0f172a99',
+ foreground: '#cbd5e1',
+ muted: '#cbd5e126',
+ caret: '#38bdf8',
+ selection: 'rgba(56,189,248,.22)',
+ selectionMatch: 'rgba(56,189,248,.14)',
+ lineHighlight: 'rgba(203,213,225,.05)',
+ gutterBackground: 'transparent',
+ gutterForeground: '#cbd5e155',
+};
+export const medianoche = createTheme({
+ theme: 'dark',
+ settings: medianocheSettings,
+ styles: [
+ { tag: [t.atom, t.bool, t.number], color: '#fbbf24' },
+ { tag: t.labelName, color: '#f472b6' },
+ { tag: t.keyword, color: '#818cf8' },
+ { tag: t.operator, color: '#38bdf8' },
+ { tag: [t.variableName, t.propertyName, t.tagName, t.typeName], color: '#7dd3fc' },
+ { tag: t.definition(t.variableName), color: '#c4b5fd' },
+ { tag: [t.string, t.special(t.string)], color: '#6ee7b7' },
+ { tag: t.comment, color: '#64748b' },
+ { tag: [t.bracket, t.unit, t.punctuation], color: '#38bdf8' },
+ { tag: [t.meta, t.attributeName], color: '#fbbf24' },
+ { tag: t.className, color: '#c4b5fd' },
+ { tag: t.invalid, color: '#f87171' },
+ ],
+});
+
+// grafito (monocromo con un solo acento ámbar — minimalista)
+export const grafitoSettings = {
+ background: '#232323',
+ lineBackground: '#23232399',
+ foreground: '#c9c9c9',
+ muted: '#c9c9c926',
+ caret: '#ffb300',
+ selection: 'rgba(255,179,0,.22)',
+ selectionMatch: 'rgba(255,179,0,.14)',
+ lineHighlight: 'rgba(201,201,201,.05)',
+ gutterBackground: 'transparent',
+ gutterForeground: '#c9c9c955',
+};
+export const grafito = createTheme({
+ theme: 'dark',
+ settings: grafitoSettings,
+ styles: [
+ { tag: [t.atom, t.bool, t.number], color: '#ffb300' },
+ { tag: t.labelName, color: '#ffb300' },
+ { tag: t.keyword, color: '#e0e0e0' },
+ { tag: t.operator, color: '#9e9e9e' },
+ { tag: [t.variableName, t.propertyName, t.tagName, t.typeName], color: '#dedede' },
+ { tag: t.definition(t.variableName), color: '#ffffff' },
+ { tag: [t.string, t.special(t.string)], color: '#ffd54f' },
+ { tag: t.comment, color: '#757575' },
+ { tag: [t.bracket, t.unit, t.punctuation], color: '#9e9e9e' },
+ { tag: [t.meta, t.attributeName], color: '#ffb300' },
+ { tag: t.className, color: '#ffffff' },
+ { tag: t.invalid, color: '#ff5252' },
+ ],
+});
diff --git a/packages/codemirror/themes/neon-madrid.mjs b/packages/codemirror/themes/neon-madrid.mjs
new file mode 100644
index 00000000..cbd05c9e
--- /dev/null
+++ b/packages/codemirror/themes/neon-madrid.mjs
@@ -0,0 +1,45 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+// Tema NEÓN: naranja neón dominante (texto de UI y código base),
+// con acentos VERDE NEÓN (números, átomos) y CIAN (operadores, corchetes, puntuación).
+export const settings = {
+ background: '#0a0a0c',
+ lineBackground: '#0a0a0c99',
+ foreground: '#ff7a18', // naranja neón — todo el texto de la interfaz y el código base
+ muted: '#ff7a1840', // bordes/separadores en naranja tenue
+ caret: '#00e5ff', // cursor cian
+ selection: 'rgba(57,255,20,0.25)', // selección verde neón
+ selectionMatch: 'rgba(0,229,255,0.22)',
+ lineHighlight: 'rgba(255,122,24,0.07)',
+ gutterBackground: 'transparent',
+ gutterForeground: '#ff7a1866',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#39ff14' }, // verde neón
+ { tag: t.labelName, color: '#00e5ff' }, // $: en cian
+ { tag: t.keyword, color: '#ff5e00' }, // const, => naranja fuerte
+ { tag: t.operator, color: '#00e5ff' }, // + - * / cian
+ { tag: t.special(t.variableName), color: '#ffb877' },
+ { tag: t.typeName, color: '#ff9a3c' },
+ { tag: t.atom, color: '#39ff14' },
+ { tag: t.number, color: '#39ff14' }, // números verde neón
+ { tag: t.definition(t.variableName), color: '#ffb877' }, // nombres de const
+ { tag: t.string, color: '#ffd59e' }, // "bd*4" "RolandTR909" ámbar cálido (legible)
+ { tag: t.special(t.string), color: '#ffd59e' },
+ { tag: t.comment, color: '#9a7a55' }, // comentarios apagados
+ { tag: t.variableName, color: '#ff9a3c' }, // s, note, stack... naranja
+ { tag: t.tagName, color: '#ff9a3c' },
+ { tag: t.bracket, color: '#00e5ff' }, // ( ) [ ] cian
+ { tag: t.meta, color: '#39ff14' },
+ { tag: t.attributeName, color: '#39ff14' },
+ { tag: t.propertyName, color: '#ff9a3c' }, // .lpf .gain .bank naranja
+ { tag: t.className, color: '#ffb877' },
+ { tag: t.invalid, color: '#ff2222' },
+ { tag: [t.unit, t.punctuation], color: '#00e5ff' }, // puntuación cian
+ ],
+});
diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs
index 59e410c5..64d28027 100644
--- a/packages/core/cyclist.mjs
+++ b/packages/core/cyclist.mjs
@@ -133,6 +133,14 @@ export class Cyclist {
this.cps = cps;
this.num_ticks_since_cps_change = 0;
}
+ // salta a un ciclo concreto (seek). El siguiente tick re-ancla la fase del
+ // reloj igual que hace un cambio de cps: num_cycles_at_cps_change tomará
+ // lastEnd (el ciclo pedido) y seconds_at_cps_change la fase actual.
+ setCycle(cycle) {
+ this.num_ticks_since_cps_change = 0;
+ this.lastBegin = cycle;
+ this.lastEnd = cycle;
+ }
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c8be1ac1..15077fda 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -19,3 +19,11 @@ catalog:
"@codemirror/view": "^6.36.2"
"@codemirror/autocomplete": "^6.18.4"
"@codemirror/search": "^6.5.8"
+
+allowBuilds:
+ '@serialport/bindings-cpp': false
+ esbuild: true
+ nx: true
+ sharp: true
+ tree-sitter: false
+ tree-sitter-haskell: false
diff --git a/website/src/i18n.mjs b/website/src/i18n.mjs
new file mode 100644
index 00000000..90ffcba2
--- /dev/null
+++ b/website/src/i18n.mjs
@@ -0,0 +1,725 @@
+// i18n ligero para la interfaz del REPL.
+// El idioma vive en settings ('language') y se cambia desde el selector del header.
+// t(key) devuelve la cadena en el idioma activo (con fallback inglés → clave).
+import { settingsMap, useSettings } from './settings.mjs';
+
+export const LANGS = {
+ es: 'Español',
+ en: 'English',
+ fr: 'Français',
+ de: 'Deutsch',
+ it: 'Italiano',
+ pt: 'Português',
+ nl: 'Nederlands',
+ pl: 'Polski',
+ cs: 'Čeština',
+ sk: 'Slovenčina',
+ ro: 'Română',
+ hu: 'Magyar',
+ el: 'Ελληνικά',
+ sv: 'Svenska',
+ da: 'Dansk',
+ no: 'Norsk',
+ fi: 'Suomi',
+ uk: 'Українська',
+};
+
+// Diccionario: clave → { idioma: texto }. Orden de idiomas igual que LANGS.
+const S = {
+ play: {
+ es: 'play',
+ en: 'play',
+ fr: 'lecture',
+ de: 'Play',
+ it: 'play',
+ pt: 'tocar',
+ nl: 'afspelen',
+ pl: 'graj',
+ cs: 'přehrát',
+ sk: 'prehrať',
+ ro: 'redare',
+ hu: 'lejátszás',
+ el: 'αναπαραγωγή',
+ sv: 'spela',
+ da: 'afspil',
+ no: 'spill',
+ fi: 'toista',
+ uk: 'грати',
+ },
+ stop: {
+ es: 'stop',
+ en: 'stop',
+ fr: 'stop',
+ de: 'Stopp',
+ it: 'stop',
+ pt: 'parar',
+ nl: 'stop',
+ pl: 'stop',
+ cs: 'stop',
+ sk: 'stop',
+ ro: 'stop',
+ hu: 'állj',
+ el: 'στοπ',
+ sv: 'stopp',
+ da: 'stop',
+ no: 'stopp',
+ fi: 'pysäytä',
+ uk: 'стоп',
+ },
+ pause: {
+ es: 'pausa',
+ en: 'pause',
+ fr: 'pause',
+ de: 'Pause',
+ it: 'pausa',
+ pt: 'pausa',
+ nl: 'pauze',
+ pl: 'pauza',
+ cs: 'pauza',
+ sk: 'pauza',
+ ro: 'pauză',
+ hu: 'szünet',
+ el: 'παύση',
+ sv: 'paus',
+ da: 'pause',
+ no: 'pause',
+ fi: 'tauko',
+ uk: 'пауза',
+ },
+ previous: {
+ es: 'anterior',
+ en: 'previous',
+ fr: 'précédent',
+ de: 'Zurück',
+ it: 'precedente',
+ pt: 'anterior',
+ nl: 'vorige',
+ pl: 'poprzedni',
+ cs: 'předchozí',
+ sk: 'predchádzajúci',
+ ro: 'anterior',
+ hu: 'előző',
+ el: 'προηγούμενο',
+ sv: 'föregående',
+ da: 'forrige',
+ no: 'forrige',
+ fi: 'edellinen',
+ uk: 'попередній',
+ },
+ next: {
+ es: 'siguiente',
+ en: 'next',
+ fr: 'suivant',
+ de: 'Weiter',
+ it: 'successivo',
+ pt: 'seguinte',
+ nl: 'volgende',
+ pl: 'następny',
+ cs: 'další',
+ sk: 'ďalší',
+ ro: 'următor',
+ hu: 'következő',
+ el: 'επόμενο',
+ sv: 'nästa',
+ da: 'næste',
+ no: 'neste',
+ fi: 'seuraava',
+ uk: 'наступний',
+ },
+ update: {
+ es: 'actualizar',
+ en: 'update',
+ fr: 'mettre à jour',
+ de: 'Aktualisieren',
+ it: 'aggiorna',
+ pt: 'atualizar',
+ nl: 'bijwerken',
+ pl: 'aktualizuj',
+ cs: 'aktualizovat',
+ sk: 'aktualizovať',
+ ro: 'actualizează',
+ hu: 'frissítés',
+ el: 'ενημέρωση',
+ sv: 'uppdatera',
+ da: 'opdater',
+ no: 'oppdater',
+ fi: 'päivitä',
+ uk: 'оновити',
+ },
+ share: {
+ es: 'compartir',
+ en: 'share',
+ fr: 'partager',
+ de: 'Teilen',
+ it: 'condividi',
+ pt: 'partilhar',
+ nl: 'delen',
+ pl: 'udostępnij',
+ cs: 'sdílet',
+ sk: 'zdieľať',
+ ro: 'partajează',
+ hu: 'megosztás',
+ el: 'κοινοποίηση',
+ sv: 'dela',
+ da: 'del',
+ no: 'del',
+ fi: 'jaa',
+ uk: 'поділитися',
+ },
+ learn: {
+ es: 'aprender',
+ en: 'learn',
+ fr: 'apprendre',
+ de: 'Lernen',
+ it: 'impara',
+ pt: 'aprender',
+ nl: 'leren',
+ pl: 'nauka',
+ cs: 'učit se',
+ sk: 'učiť sa',
+ ro: 'învață',
+ hu: 'tanulás',
+ el: 'μάθηση',
+ sv: 'lär dig',
+ da: 'lær',
+ no: 'lær',
+ fi: 'opi',
+ uk: 'вчитися',
+ },
+ // pestañas del panel
+ tabProjects: {
+ es: 'projects',
+ en: 'projects',
+ fr: 'projets',
+ de: 'Projekte',
+ it: 'progetti',
+ pt: 'projetos',
+ nl: 'projecten',
+ pl: 'projekty',
+ cs: 'projekty',
+ sk: 'projekty',
+ ro: 'proiecte',
+ hu: 'projektek',
+ el: 'έργα',
+ sv: 'projekt',
+ da: 'projekter',
+ no: 'prosjekter',
+ fi: 'projektit',
+ uk: 'проєкти',
+ },
+ tabGuide: {
+ es: 'guía',
+ en: 'guide',
+ fr: 'guide',
+ de: 'Anleitung',
+ it: 'guida',
+ pt: 'guia',
+ nl: 'gids',
+ pl: 'przewodnik',
+ cs: 'průvodce',
+ sk: 'sprievodca',
+ ro: 'ghid',
+ hu: 'útmutató',
+ el: 'οδηγός',
+ sv: 'guide',
+ da: 'guide',
+ no: 'guide',
+ fi: 'opas',
+ uk: 'посібник',
+ },
+ tabIntro: {
+ es: 'intro',
+ en: 'intro',
+ fr: 'intro',
+ de: 'Intro',
+ it: 'intro',
+ pt: 'intro',
+ nl: 'intro',
+ pl: 'intro',
+ cs: 'úvod',
+ sk: 'úvod',
+ ro: 'intro',
+ hu: 'intro',
+ el: 'εισαγωγή',
+ sv: 'intro',
+ da: 'intro',
+ no: 'intro',
+ fi: 'intro',
+ uk: 'вступ',
+ },
+ tabPatterns: {
+ es: 'patterns',
+ en: 'patterns',
+ fr: 'motifs',
+ de: 'Patterns',
+ it: 'pattern',
+ pt: 'padrões',
+ nl: 'patronen',
+ pl: 'wzorce',
+ cs: 'vzory',
+ sk: 'vzory',
+ ro: 'tipare',
+ hu: 'minták',
+ el: 'μοτίβα',
+ sv: 'mönster',
+ da: 'mønstre',
+ no: 'mønstre',
+ fi: 'kuviot',
+ uk: 'патерни',
+ },
+ tabSounds: {
+ es: 'sonidos',
+ en: 'sounds',
+ fr: 'sons',
+ de: 'Klänge',
+ it: 'suoni',
+ pt: 'sons',
+ nl: 'geluiden',
+ pl: 'dźwięki',
+ cs: 'zvuky',
+ sk: 'zvuky',
+ ro: 'sunete',
+ hu: 'hangok',
+ el: 'ήχοι',
+ sv: 'ljud',
+ da: 'lyde',
+ no: 'lyder',
+ fi: 'äänet',
+ uk: 'звуки',
+ },
+ tabReference: {
+ es: 'referencia',
+ en: 'reference',
+ fr: 'référence',
+ de: 'Referenz',
+ it: 'riferimento',
+ pt: 'referência',
+ nl: 'referentie',
+ pl: 'odniesienie',
+ cs: 'reference',
+ sk: 'referencia',
+ ro: 'referință',
+ hu: 'referencia',
+ el: 'αναφορά',
+ sv: 'referens',
+ da: 'reference',
+ no: 'referanse',
+ fi: 'viite',
+ uk: 'довідник',
+ },
+ tabExport: {
+ es: 'exportar',
+ en: 'export',
+ fr: 'exporter',
+ de: 'Export',
+ it: 'esporta',
+ pt: 'exportar',
+ nl: 'exporteren',
+ pl: 'eksport',
+ cs: 'export',
+ sk: 'export',
+ ro: 'export',
+ hu: 'exportálás',
+ el: 'εξαγωγή',
+ sv: 'exportera',
+ da: 'eksport',
+ no: 'eksport',
+ fi: 'vie',
+ uk: 'експорт',
+ },
+ tabConsole: {
+ es: 'consola',
+ en: 'console',
+ fr: 'console',
+ de: 'Konsole',
+ it: 'console',
+ pt: 'consola',
+ nl: 'console',
+ pl: 'konsola',
+ cs: 'konzole',
+ sk: 'konzola',
+ ro: 'consolă',
+ hu: 'konzol',
+ el: 'κονσόλα',
+ sv: 'konsol',
+ da: 'konsol',
+ no: 'konsoll',
+ fi: 'konsoli',
+ uk: 'консоль',
+ },
+ tabSettings: {
+ es: 'ajustes',
+ en: 'settings',
+ fr: 'réglages',
+ de: 'Einstellungen',
+ it: 'impostazioni',
+ pt: 'definições',
+ nl: 'instellingen',
+ pl: 'ustawienia',
+ cs: 'nastavení',
+ sk: 'nastavenia',
+ ro: 'setări',
+ hu: 'beállítások',
+ el: 'ρυθμίσεις',
+ sv: 'inställningar',
+ da: 'indstillinger',
+ no: 'innstillinger',
+ fi: 'asetukset',
+ uk: 'налаштування',
+ },
+ tabFiles: {
+ es: 'archivos',
+ en: 'files',
+ fr: 'fichiers',
+ de: 'Dateien',
+ it: 'file',
+ pt: 'ficheiros',
+ nl: 'bestanden',
+ pl: 'pliki',
+ cs: 'soubory',
+ sk: 'súbory',
+ ro: 'fișiere',
+ hu: 'fájlok',
+ el: 'αρχεία',
+ sv: 'filer',
+ da: 'filer',
+ no: 'filer',
+ fi: 'tiedostot',
+ uk: 'файли',
+ },
+ // transporte
+ seekHint: {
+ es: 'click o arrastra para saltar a ese punto del loop',
+ en: 'click or drag to jump to that point of the loop',
+ fr: 'cliquez ou glissez pour sauter à ce point de la boucle',
+ de: 'Klicken oder ziehen, um zu diesem Punkt des Loops zu springen',
+ it: 'clicca o trascina per saltare a quel punto del loop',
+ pt: 'clica ou arrasta para saltar para esse ponto do loop',
+ nl: 'klik of sleep om naar dat punt van de loop te springen',
+ pl: 'kliknij lub przeciągnij, aby skoczyć do tego punktu pętli',
+ cs: 'kliknutím nebo tažením skočíte na dané místo smyčky',
+ sk: 'kliknutím alebo ťahaním skočíte na dané miesto slučky',
+ ro: 'clic sau trage pentru a sări la acel punct al buclei',
+ hu: 'kattints vagy húzd a hurok adott pontjára ugráshoz',
+ el: 'κλικ ή σύρσιμο για μετάβαση σε αυτό το σημείο του loop',
+ sv: 'klicka eller dra för att hoppa till den punkten i loopen',
+ da: 'klik eller træk for at hoppe til det punkt i loopet',
+ no: 'klikk eller dra for å hoppe til det punktet i loopen',
+ fi: 'klikkaa tai vedä hypätäksesi siihen kohtaan luuppia',
+ uk: 'клацніть або перетягніть, щоб перейти до цієї точки циклу',
+ },
+ resizeHint: {
+ es: 'arrastra para redimensionar',
+ en: 'drag to resize',
+ fr: 'glissez pour redimensionner',
+ de: 'Zum Anpassen ziehen',
+ it: 'trascina per ridimensionare',
+ pt: 'arrasta para redimensionar',
+ nl: 'sleep om te vergroten/verkleinen',
+ pl: 'przeciągnij, aby zmienić rozmiar',
+ cs: 'tažením změníte velikost',
+ sk: 'ťahaním zmeníte veľkosť',
+ ro: 'trage pentru redimensionare',
+ hu: 'húzd az átméretezéshez',
+ el: 'σύρετε για αλλαγή μεγέθους',
+ sv: 'dra för att ändra storlek',
+ da: 'træk for at ændre størrelse',
+ no: 'dra for å endre størrelse',
+ fi: 'vedä muuttaaksesi kokoa',
+ uk: 'перетягніть, щоб змінити розмір',
+ },
+ cycles: {
+ es: 'ciclos',
+ en: 'cycles',
+ fr: 'cycles',
+ de: 'Zyklen',
+ it: 'cicli',
+ pt: 'ciclos',
+ nl: 'cycli',
+ pl: 'cykle',
+ cs: 'cykly',
+ sk: 'cykly',
+ ro: 'cicluri',
+ hu: 'ciklusok',
+ el: 'κύκλοι',
+ sv: 'cykler',
+ da: 'cyklusser',
+ no: 'sykluser',
+ fi: 'syklit',
+ uk: 'цикли',
+ },
+ // projects
+ save: {
+ es: 'guardar',
+ en: 'save',
+ fr: 'enregistrer',
+ de: 'Speichern',
+ it: 'salva',
+ pt: 'guardar',
+ nl: 'opslaan',
+ pl: 'zapisz',
+ cs: 'uložit',
+ sk: 'uložiť',
+ ro: 'salvează',
+ hu: 'mentés',
+ el: 'αποθήκευση',
+ sv: 'spara',
+ da: 'gem',
+ no: 'lagre',
+ fi: 'tallenna',
+ uk: 'зберегти',
+ },
+ projectPlaceholder: {
+ es: 'nombre del proyecto (o carpeta/nombre)...',
+ en: 'project name (or folder/name)...',
+ fr: 'nom du projet (ou dossier/nom)...',
+ de: 'Projektname (oder Ordner/Name)...',
+ it: 'nome del progetto (o cartella/nome)...',
+ pt: 'nome do projeto (ou pasta/nome)...',
+ nl: 'projectnaam (of map/naam)...',
+ pl: 'nazwa projektu (lub folder/nazwa)...',
+ cs: 'název projektu (nebo složka/název)...',
+ sk: 'názov projektu (alebo priečinok/názov)...',
+ ro: 'numele proiectului (sau dosar/nume)...',
+ hu: 'projekt neve (vagy mappa/név)...',
+ el: 'όνομα έργου (ή φάκελος/όνομα)...',
+ sv: 'projektnamn (eller mapp/namn)...',
+ da: 'projektnavn (eller mappe/navn)...',
+ no: 'prosjektnavn (eller mappe/navn)...',
+ fi: 'projektin nimi (tai kansio/nimi)...',
+ uk: 'назва проєкту (або папка/назва)...',
+ },
+ copy: {
+ es: 'copiar',
+ en: 'copy',
+ fr: 'copier',
+ de: 'Kopieren',
+ it: 'copia',
+ pt: 'copiar',
+ nl: 'kopiëren',
+ pl: 'kopiuj',
+ cs: 'kopírovat',
+ sk: 'kopírovať',
+ ro: 'copiază',
+ hu: 'másolás',
+ el: 'αντιγραφή',
+ sv: 'kopiera',
+ da: 'kopiér',
+ no: 'kopier',
+ fi: 'kopioi',
+ uk: 'копіювати',
+ },
+ copied: {
+ es: 'copiado',
+ en: 'copied',
+ fr: 'copié',
+ de: 'Kopiert',
+ it: 'copiato',
+ pt: 'copiado',
+ nl: 'gekopieerd',
+ pl: 'skopiowano',
+ cs: 'zkopírováno',
+ sk: 'skopírované',
+ ro: 'copiat',
+ hu: 'másolva',
+ el: 'αντιγράφηκε',
+ sv: 'kopierad',
+ da: 'kopieret',
+ no: 'kopiert',
+ fi: 'kopioitu',
+ uk: 'скопійовано',
+ },
+ load: {
+ es: 'cargar',
+ en: 'load',
+ fr: 'charger',
+ de: 'Laden',
+ it: 'carica',
+ pt: 'carregar',
+ nl: 'laden',
+ pl: 'wczytaj',
+ cs: 'načíst',
+ sk: 'načítať',
+ ro: 'încarcă',
+ hu: 'betöltés',
+ el: 'φόρτωση',
+ sv: 'ladda',
+ da: 'indlæs',
+ no: 'last inn',
+ fi: 'lataa',
+ uk: 'завантажити',
+ },
+ read: {
+ es: 'leer',
+ en: 'read',
+ fr: 'lire',
+ de: 'Lesen',
+ it: 'leggi',
+ pt: 'ler',
+ nl: 'lezen',
+ pl: 'czytaj',
+ cs: 'číst',
+ sk: 'čítať',
+ ro: 'citește',
+ hu: 'olvasás',
+ el: 'ανάγνωση',
+ sv: 'läs',
+ da: 'læs',
+ no: 'les',
+ fi: 'lue',
+ uk: 'читати',
+ },
+ myProjects: {
+ es: 'mis proyectos',
+ en: 'my projects',
+ fr: 'mes projets',
+ de: 'meine Projekte',
+ it: 'i miei progetti',
+ pt: 'os meus projetos',
+ nl: 'mijn projecten',
+ pl: 'moje projekty',
+ cs: 'moje projekty',
+ sk: 'moje projekty',
+ ro: 'proiectele mele',
+ hu: 'projektjeim',
+ el: 'τα έργα μου',
+ sv: 'mina projekt',
+ da: 'mine projekter',
+ no: 'mine prosjekter',
+ fi: 'omat projektit',
+ uk: 'мої проєкти',
+ },
+ noProjects: {
+ es: 'Aún no hay proyectos. Escribe un nombre y pulsa guardar.',
+ en: 'No projects yet. Type a name and press save.',
+ fr: 'Pas encore de projets. Saisissez un nom et appuyez sur enregistrer.',
+ de: 'Noch keine Projekte. Namen eingeben und Speichern drücken.',
+ it: 'Ancora nessun progetto. Scrivi un nome e premi salva.',
+ pt: 'Ainda não há projetos. Escreve um nome e carrega em guardar.',
+ nl: 'Nog geen projecten. Typ een naam en druk op opslaan.',
+ pl: 'Brak projektów. Wpisz nazwę i naciśnij zapisz.',
+ cs: 'Zatím žádné projekty. Zadejte název a stiskněte uložit.',
+ sk: 'Zatiaľ žiadne projekty. Zadajte názov a stlačte uložiť.',
+ ro: 'Încă nu există proiecte. Scrie un nume și apasă salvează.',
+ hu: 'Még nincsenek projektek. Írj be egy nevet és nyomd meg a mentést.',
+ el: 'Δεν υπάρχουν έργα ακόμα. Γράψτε ένα όνομα και πατήστε αποθήκευση.',
+ sv: 'Inga projekt än. Skriv ett namn och tryck på spara.',
+ da: 'Ingen projekter endnu. Skriv et navn og tryk på gem.',
+ no: 'Ingen prosjekter ennå. Skriv et navn og trykk lagre.',
+ fi: 'Ei vielä projekteja. Kirjoita nimi ja paina tallenna.',
+ uk: 'Проєктів ще немає. Введіть назву й натисніть зберегти.',
+ },
+ previewTitle: {
+ es: 'vista previa',
+ en: 'preview',
+ fr: 'aperçu',
+ de: 'Vorschau',
+ it: 'anteprima',
+ pt: 'pré-visualização',
+ nl: 'voorbeeld',
+ pl: 'podgląd',
+ cs: 'náhled',
+ sk: 'náhľad',
+ ro: 'previzualizare',
+ hu: 'előnézet',
+ el: 'προεπισκόπηση',
+ sv: 'förhandsvisning',
+ da: 'forhåndsvisning',
+ no: 'forhåndsvisning',
+ fi: 'esikatselu',
+ uk: 'попередній перегляд',
+ },
+ loadInEditor: {
+ es: 'cargar en editor',
+ en: 'load in editor',
+ fr: "charger dans l'éditeur",
+ de: 'In Editor laden',
+ it: "carica nell'editor",
+ pt: 'carregar no editor',
+ nl: 'laden in editor',
+ pl: 'wczytaj do edytora',
+ cs: 'načíst do editoru',
+ sk: 'načítať do editora',
+ ro: 'încarcă în editor',
+ hu: 'betöltés a szerkesztőbe',
+ el: 'φόρτωση στον επεξεργαστή',
+ sv: 'ladda i editorn',
+ da: 'indlæs i editor',
+ no: 'last inn i editor',
+ fi: 'lataa editoriin',
+ uk: 'завантажити в редактор',
+ },
+ previewHelp: {
+ es: 'Haz click en el nombre de un tema para leer su código aquí. No se ejecuta ni toca lo que está sonando — solo «cargar» reemplaza el editor.',
+ en: 'Click a track name to read its code here. It does not run nor touch what is playing — only "load" replaces the editor.',
+ fr: "Cliquez sur le nom d'un morceau pour lire son code ici. Rien ne s'exécute — seul « charger » remplace l'éditeur.",
+ de: 'Klicke auf einen Tracknamen, um seinen Code hier zu lesen. Nichts wird ausgeführt — nur „Laden" ersetzt den Editor.',
+ it: "Clicca sul nome di un brano per leggerne il codice qui. Non viene eseguito — solo «carica» sostituisce l'editor.",
+ pt: 'Clica no nome de uma faixa para ler o seu código aqui. Nada é executado — só «carregar» substitui o editor.',
+ nl: 'Klik op een tracknaam om de code hier te lezen. Er wordt niets uitgevoerd — alleen "laden" vervangt de editor.',
+ pl: 'Kliknij nazwę utworu, aby przeczytać jego kod. Nic się nie uruchamia — tylko „wczytaj" zastępuje edytor.',
+ cs: 'Klikněte na název skladby a přečtěte si zde její kód. Nic se nespustí — jen „načíst" nahradí editor.',
+ sk: 'Kliknite na názov skladby a prečítajte si tu jej kód. Nič sa nespustí — len „načítať" nahradí editor.',
+ ro: 'Dă clic pe numele unei piese pentru a-i citi codul aici. Nimic nu rulează — doar „încarcă" înlocuiește editorul.',
+ hu: 'Kattints egy szám nevére, hogy itt elolvasd a kódját. Semmi sem fut le — csak a „betöltés" cseréli le a szerkesztőt.',
+ el: 'Κάντε κλικ στο όνομα ενός κομματιού για να δείτε τον κώδικά του εδώ. Δεν εκτελείται τίποτα — μόνο η «φόρτωση» αντικαθιστά τον επεξεργαστή.',
+ sv: 'Klicka på ett spårnamn för att läsa dess kod här. Inget körs — bara "ladda" ersätter editorn.',
+ da: 'Klik på et nummernavn for at læse dets kode her. Intet kører — kun "indlæs" erstatter editoren.',
+ no: 'Klikk på et spornavn for å lese koden her. Ingenting kjøres — bare "last inn" erstatter editoren.',
+ fi: 'Klikkaa kappaleen nimeä lukeaksesi sen koodin tässä. Mitään ei suoriteta — vain "lataa" korvaa editorin.',
+ uk: 'Клацніть назву треку, щоб прочитати його код тут. Нічого не виконується — лише «завантажити» замінює редактор.',
+ },
+ theme: {
+ es: 'tema',
+ en: 'theme',
+ fr: 'thème',
+ de: 'Thema',
+ it: 'tema',
+ pt: 'tema',
+ nl: 'thema',
+ pl: 'motyw',
+ cs: 'motiv',
+ sk: 'motív',
+ ro: 'temă',
+ hu: 'téma',
+ el: 'θέμα',
+ sv: 'tema',
+ da: 'tema',
+ no: 'tema',
+ fi: 'teema',
+ uk: 'тема',
+ },
+ language: {
+ es: 'idioma',
+ en: 'language',
+ fr: 'langue',
+ de: 'Sprache',
+ it: 'lingua',
+ pt: 'idioma',
+ nl: 'taal',
+ pl: 'język',
+ cs: 'jazyk',
+ sk: 'jazyk',
+ ro: 'limbă',
+ hu: 'nyelv',
+ el: 'γλώσσα',
+ sv: 'språk',
+ da: 'sprog',
+ no: 'språk',
+ fi: 'kieli',
+ uk: 'мова',
+ },
+};
+
+export const getLang = () => settingsMap.get().language || 'es';
+
+export const translate = (key, lang) => S[key]?.[lang] ?? S[key]?.en ?? key;
+
+// hook de React: re-renderiza al cambiar el idioma en settings
+export function useT() {
+ const { language } = useSettings();
+ const lang = language || 'es';
+ return (key) => translate(key, lang);
+}
+
+export const setLanguage = (lang) => settingsMap.setKey('language', lang);
diff --git a/website/src/repl/components/ReplEditor.jsx b/website/src/repl/components/ReplEditor.jsx
index 437475e6..3db15c90 100644
--- a/website/src/repl/components/ReplEditor.jsx
+++ b/website/src/repl/components/ReplEditor.jsx
@@ -1,6 +1,7 @@
import { Code } from '@src/repl/components/Code';
import Loader from '@src/repl/components/Loader';
import { BottomPanel, MainPanel, RightPanel } from '@src/repl/components/panel/Panel';
+import { TransportBar } from '@src/repl/components/TransportBar';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { useSettings } from '@src/settings.mjs';
@@ -22,7 +23,11 @@ export default function ReplEditor(Props) {
{/*
+ {/* editor (izquierda) + barra de transporte FIJA debajo del código */}
+
+ {!isZen && | {headL} | +{headR} | +
|---|---|
| {a} | +{b} | +
+ Guía rápida de sonidos para construir y pinchar. Todo esto se combina con . encadenando:
+ p.ej. s("bd*4").bank("RolandTR909").lpf(800).
+
+ Se usan con los sonidos de batería: s("bd sd hh cp").bank("RolandTR808"). + Las más icónicas: +
++ …y ~50 más: Roland (MC202, MC303, R8, TR626…), Korg (KPR77, DDM110, M1…), Yamaha (RY30, TG33…), Emu, Alesis, + Sequential, Simmons, Casio, Moog, Serge… Prueba cualquiera. +
+
+ Con notas: note("c3 eb3 g3").s("gm_epiano2").room(.4).
+ Soundfonts General MIDI (128 instrumentos, prefijo gm_) + samples reales:
+
+ Más en la pestaña reference (todas las funciones) y en sounds (explorar samples cargados). +
+