From 7b9f1155b408e73890dd8f992f16bd4b7656f715 Mon Sep 17 00:00:00 2001 From: hacklab Date: Fri, 17 Jul 2026 01:47:12 +0200 Subject: [PATCH] =?UTF-8?q?Mejoras=20hacklab:=20i18n,=20pesta=C3=B1a=20Pro?= =?UTF-8?q?jects,=20transporte=20con=20seek,=20gu=C3=ADa=20y=20temas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - i18n ligero de la interfaz del REPL (castellano, inglés, francés, alemán, italiano, portugués...) con selector en el header - Pestaña Projects: gestor local de proyectos con carpetas, buscador y librería de tracks de fábrica (bases techno/acid originales) - Barra de transporte bajo el editor: play/pausa/anterior/siguiente, tiempo y barra de progreso arrastrable; seek real vía Cyclist.setCycle() - Pestaña Guía: chuleta de síntesis y sonidos para el directo - Packs de temas del editor: neon-madrid + hacklab-pack (5 neón + 5 estándar) - pnpm-workspace: allowBuilds para instalación limpia en Linux --- packages/codemirror/themes.mjs | 45 ++ packages/codemirror/themes/hacklab-pack.mjs | 240 ++++++ packages/codemirror/themes/neon-madrid.mjs | 45 ++ packages/core/cyclist.mjs | 8 + pnpm-workspace.yaml | 8 + website/src/i18n.mjs | 725 ++++++++++++++++++ website/src/repl/components/ReplEditor.jsx | 7 +- website/src/repl/components/TransportBar.jsx | 219 ++++++ .../src/repl/components/panel/GuideTab.jsx | 131 ++++ website/src/repl/components/panel/Panel.jsx | 276 +++++-- .../src/repl/components/panel/ProjectsTab.jsx | 425 ++++++++++ .../components/panel/default-projects.mjs | 654 ++++++++++++++++ website/src/settings.mjs | 3 +- 13 files changed, 2731 insertions(+), 55 deletions(-) create mode 100644 packages/codemirror/themes/hacklab-pack.mjs create mode 100644 packages/codemirror/themes/neon-madrid.mjs create mode 100644 website/src/i18n.mjs create mode 100644 website/src/repl/components/TransportBar.jsx create mode 100644 website/src/repl/components/panel/GuideTab.jsx create mode 100644 website/src/repl/components/panel/ProjectsTab.jsx create mode 100644 website/src/repl/components/panel/default-projects.mjs 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 && } +
{!isZen && panelPosition === 'right' && }
diff --git a/website/src/repl/components/TransportBar.jsx b/website/src/repl/components/TransportBar.jsx new file mode 100644 index 00000000..cf8ad3a9 --- /dev/null +++ b/website/src/repl/components/TransportBar.jsx @@ -0,0 +1,219 @@ +import { useStore } from '@nanostores/react'; +import { useEffect, useRef, useState } from 'react'; +import cx from '@src/cx.mjs'; +import { useT } from '@src/i18n.mjs'; +import { $projects, $playing, parseMeta, fmt } from './panel/ProjectsTab.jsx'; + +// Barra de transporte estilo Spotify, FIJA debajo del editor de código: +// · fila superior: controles CENTRADOS (anterior / play-pausa / siguiente) +// · fila inferior: tiempo transcurrido · barra ARRASTRABLE (seek) · duración +// La barra permite saltar a un segundo concreto del loop (click o arrastre; +// el salto se aplica al soltar). Pausar recuerda la posición y reanudar +// continúa desde ahí (scheduler.setCycle). Arrastrar con el tema parado fija +// el punto desde el que arrancará el play. +export function TransportBar({ context }) { + const projects = useStore($projects); + const playingName = useStore($playing); + const [, tick] = useState(0); + const t = useT(); + const [dragFrac, setDragFrac] = useState(null); // fracción bajo el dedo mientras se arrastra + const pausedAtRef = useRef(null); // ciclo (dentro del loop) donde se pausó + const barRef = useRef(null); + + // refresco de la barra (~8 fps) + useEffect(() => { + const id = setInterval(() => tick((t) => (t + 1) % 1e9), 120); + return () => clearInterval(id); + }, []); + + // al cambiar de tema se descarta la posición de pausa (era de otro loop) + useEffect(() => { + pausedAtRef.current = null; + }, [playingName]); + + const editor = () => context.editorRef.current; + const ed = editor(); + const sch = ed?.repl?.scheduler; + const started = !!sch?.started; + + // longitud del loop = del código que está REALMENTE en el editor (lo que suena) + const meta = parseMeta(ed?.code ?? ''); + const loopCycles = Math.max(meta.loopCycles || 1, 1e-6); + const cps = (started && sch?.cps ? sch.cps : meta.cps) || 0.5; + + // posición dentro del loop: sonando → scheduler; pausado → posición recordada; + // arrastrando → manda el dedo (solo visual hasta soltar) + const pausedAt = pausedAtRef.current; + const nowCycle = started ? (sch?.now?.() ?? 0) : (pausedAt ?? 0); + const posCycle = ((nowCycle % loopCycles) + loopCycles) % loopCycles; + const frac = dragFrac ?? posCycle / loopCycles; + const shownCycle = dragFrac != null ? dragFrac * loopCycles : posCycle; + const totalSec = loopCycles / cps; + const elapsedSec = frac * totalSec; + const pct = `${(Math.min(Math.max(frac, 0), 1) * 100).toFixed(1)}%`; + + const displayed = projects.slice().reverse(); + const curIdx = displayed.findIndex((x) => x.name === playingName); + + // ── SEEK: saltar a un punto concreto del loop ── + const seekTo = (f) => { + const e = editor(); + if (!e) return; + const target = Math.min(Math.max(f, 0), 0.999) * loopCycles; + const s = e.repl?.scheduler; + if (s?.started) { + // mantenemos la iteración actual del arrange y movemos solo la fase + const base = Math.floor((s.now?.() ?? 0) / loopCycles) * loopCycles; + s.setCycle?.(base + target); + } else { + // parado/pausado: recordar el punto; play arrancará desde ahí + pausedAtRef.current = target; + } + }; + + const togglePlay = async () => { + const e = editor(); + if (!e) return; + const s = e.repl?.scheduler; + if (s?.started) { + // PAUSA: recuerda dónde íbamos (dentro del loop) y para el audio + const now = s.now?.() ?? 0; + pausedAtRef.current = ((now % loopCycles) + loopCycles) % loopCycles; + e.stop(); + } else { + // PLAY: si hay posición recordada (pausa o seek en parado), reanuda ahí + const resume = pausedAtRef.current; + pausedAtRef.current = null; + await e.evaluate(); + if (resume != null) e.repl?.scheduler?.setCycle?.(resume); + } + }; + + const skip = (dir) => { + const e = editor(); + if (!e || !displayed.length) return; + pausedAtRef.current = null; // cambiar de tema descarta la posición pausada + const base = curIdx < 0 ? (dir > 0 ? -1 : 0) : curIdx; + const idx = (base + dir + displayed.length) % displayed.length; + const p = displayed[idx]; + e.setCode(p.code); + $playing.set(p.name); + e.evaluate(); + }; + + // ── scrubbing con puntero (click o arrastre; el salto se aplica al soltar) ── + const fracFromEvent = (ev) => { + const el = barRef.current; + if (!el) return 0; + const r = el.getBoundingClientRect(); + return Math.min(Math.max((ev.clientX - r.left) / r.width, 0), 1); + }; + const onPointerDown = (ev) => { + ev.preventDefault(); + ev.currentTarget.setPointerCapture?.(ev.pointerId); + setDragFrac(fracFromEvent(ev)); + }; + const onPointerMove = (ev) => { + if (dragFrac == null) return; + setDragFrac(fracFromEvent(ev)); + }; + const onPointerUp = (ev) => { + if (dragFrac == null) return; + const f = fracFromEvent(ev); + setDragFrac(null); + seekTo(f); + }; + + // 5 marcas de ciclo debajo de la barra (0 … total) + const marks = [0, 1, 2, 3, 4].map((i) => Math.round((loopCycles * i) / 4)); + + const circle = + 'rounded-full border-2 border-muted flex items-center justify-center text-foreground hover:bg-cyan-400/10 hover:border-cyan-400/70 transition-colors shrink-0'; + + return ( +
+ {/* fila 1: controles CENTRADOS (estilo Spotify) */} +
+ + + +
+ + {/* fila 2: transcurrido · BARRA ARRASTRABLE · duración */} +
+
{fmt(elapsedSec)}
+
+
+ {/* número flotante = ciclo actual (sigue al dedo al arrastrar) */} +
+ {Math.floor(shownCycle)} +
+ {/* pista clicable / arrastrable */} +
+
+
+ {/* tirador (visible al pasar el ratón o arrastrar) */} +
+
+
+ {/* números de ciclo a lo largo de la barra */} +
+ {marks.map((label, i) => ( + 0 && i < 4 && '-translate-x-1/2', + )} + style={i > 0 && i < 4 ? { left: `${i * 25}%` } : undefined} + > + {label} + + ))} +
+
+
+
+
{fmt(totalSec)}
+
+ {Math.floor(shownCycle)}/{Math.round(loopCycles)} {t('cycles')} +
+
+
+
+ ); +} diff --git a/website/src/repl/components/panel/GuideTab.jsx b/website/src/repl/components/panel/GuideTab.jsx new file mode 100644 index 00000000..e86ecfdf --- /dev/null +++ b/website/src/repl/components/panel/GuideTab.jsx @@ -0,0 +1,131 @@ +import cx from '@src/cx.mjs'; +import { useSettings } from '@src/settings.mjs'; + +// Documentación rápida de sonidos para tener a mano mientras se pincha. +const SYNTHS = [ + ['sawtooth', 'la onda del ácido — rica en armónicos, brilla al filtrarla'], + ['square / pulse', 'hueca y nasal — leads, bleeps, acid alternativo'], + ['triangle', 'suave, casi seno — sub y tonos redondos'], + ['sine', 'seno puro — sub-bajo limpio bajo el ácido'], + ['supersaw', 'muro de sierras — stabs de rave, hoover, trance'], + ['.fm(i).fmh(r)', 'síntesis FM — campanas, metales, bajos tipo DX'], + ['white / pink / brown', 'ruido — risers, hats, texturas, sweeps'], +]; + +const DRUMS = [ + ['RolandTR909', 'techno/house — bombo potente, hats metálicos. LA caja del techno'], + ['RolandTR808', 'electro/hip-hop — sub-bombo largo, cowbell, claps secos'], + ['RolandTR707', 'house — limpia y seca, muy digital 80s'], + ['RolandTR606', 'acid/electro — pequeña, hats finos (pareja de la TB-303)'], + ['RolandTR727', 'percusión latina — congas, bongos, timbales'], + ['RolandTR505', 'synthpop — samples cortos y funcionales'], + ['LinnDrum', 'pop 80s — samples reales gated (Prince, Depeche Mode)'], + ['LinnLM1', 'el primer drum-sampler icónico'], + ['OberheimDMX', 'hip-hop old school, electro-funk'], + ['EmuSP12 / AkaiMPC60', 'boom-bap — swing y crujido legendarios'], + ['CasioRZ1 / CasioSK1', 'lo-fi barato con mucho carácter'], + ['KorgKR55 / KorgMinipops', 'preset-box vintage, dub y exotica (Boards of Canada)'], + ['SimmonsSDS5', 'toms electrónicos 80s (pium-pium)'], + ['YamahaRX5 / RX21', 'FM digital 80s'], + ['BossDR110 / DR55', 'analógicas minúsculas, hats brillantes'], + ['RhythmAce', 'órgano-ritmo vintage, muy lo-fi'], +]; + +const INSTR = [ + ['piano', 'piano acústico sampleado (real, muy bueno)'], + ['gm_epiano1 · gm_epiano2', 'Rhodes / e-piano — detroit, deep house'], + ['gm_acoustic_piano · gm_bright_acoustic_piano', 'pianos GM'], + ['gm_pad_warm · gm_pad_halo · gm_pad_new_age', 'pads atmosféricos que envuelven'], + ['gm_choir_aahs · gm_voice_oohs · gm_synth_choir', 'coros y voces etéreas'], + ['gm_string_ensemble_1 · gm_synth_strings_1', 'cuerdas'], + ['gm_vibraphone · gm_marimba · gm_xylophone · gm_kalimba', 'láminas/mallets cristalinos'], + ['gm_church_organ · gm_drawbar_organ', 'órganos'], + ['gm_synth_bass_1 · gm_acoustic_bass', 'bajos melódicos'], + ['gm_lead_1_square · gm_lead_2_sawtooth', 'leads de synth'], +]; + +const SYNTAX = [ + ['.bank("RolandTR909")', 'elige la caja de ritmos (¡71 disponibles!)'], + ['s("bd sd hh oh cp rim lt mt ht")', 'sonidos de batería'], + ['note("c2 eb2 g2") · n("0 3 5").scale("C:minor")', 'notas / grados de escala'], + ['.lpf() .lpq() .lpenv() .lpd()', 'filtro: corte · resonancia · envolvente · decay = EL ÁCIDO'], + ['.distort("2:.4") .crush() .shape()', 'suciedad: distorsión (drive:volumen), bitcrush'], + ['.room() .delay() .dt() .dfb()', 'espacio: reverb · delay (tiempo, feedback)'], + ['.mask("<1 1 0 1>")', 'que algo aparezca/desaparezca por ciclos'], + ['.every(8, x=>...) .degradeBy() .ply() .chunk()', 'variación y mutación'], + ['sine.range(300,3000).slow(32)', 'movimiento lento y continuo (respira)'], +]; + +function Table({ rows, headL, headR }) { + return ( + + + + + + + + + {rows.map(([a, b]) => ( + + + + + ))} + +
{headL}{headR}
{a}{b}
+ ); +} + +function Section({ title, children }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +export function GuideTab() { + const { fontFamily } = useSettings(); + return ( +
+

+ 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). +

+ + ); +} diff --git a/website/src/repl/components/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx index b36ae3ee..4984c85c 100644 --- a/website/src/repl/components/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -1,13 +1,18 @@ import { Bars3Icon, PlayIcon, StopIcon, XMarkIcon } from '@heroicons/react/16/solid'; +import { useState } from 'react'; import cx from '@src/cx.mjs'; +import { themes } from '@strudel/codemirror'; +import { LANGS, useT, setLanguage } from '@src/i18n.mjs'; import { StrudelIcon } from '@src/repl/components/icons/StrudelIcon'; -import { useSettings, setIsZen, setIsPanelOpened, setActiveFooter as setTab } from '../../../settings.mjs'; +import { useSettings, setIsZen, setIsPanelOpened, setActiveFooter as setTab, settingsMap } from '../../../settings.mjs'; import '../../Repl.css'; import { useLogger } from '../useLogger'; import { ConsoleTab } from './ConsoleTab'; import ExportTab from './ExportTab'; import { FilesTab } from './FilesTab'; import { PatternsTab } from './PatternsTab'; +import { ProjectsTab } from './ProjectsTab'; +import { GuideTab } from './GuideTab'; import { Reference } from './Reference'; import { SettingsTab } from './SettingsTab'; import { SoundsTab } from './SoundsTab'; @@ -67,16 +72,16 @@ export function MainPanel({ context, isEmbedded = false, className }) {