diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx
index 77a3efbc..b27f11c0 100644
--- a/website/src/repl/Footer.jsx
+++ b/website/src/repl/Footer.jsx
@@ -1,15 +1,15 @@
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
import { logger } from '@strudel.cycles/core';
-import { useEvent, cx } from '@strudel.cycles/react';
-// import { cx } from '@strudel.cycles/react';
+import { cx, useEvent } from '@strudel.cycles/react';
import { nanoid } from 'nanoid';
-import React, { useMemo, useCallback, useLayoutEffect, useRef, useState } from 'react';
-import { Reference } from './Reference';
-import { themes } from './themes.mjs';
-import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
-import { getAudioContext, soundMap } from '@strudel.cycles/webaudio';
-import { useStore } from '@nanostores/react';
-import { FilesTab } from './FilesTab';
+import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
+import { setActiveFooter, useSettings } from '../settings.mjs';
+import { ConsoleTab } from './panel/ConsoleTab';
+import { FilesTab } from './panel/FilesTab';
+import { Reference } from './panel/Reference';
+import { SettingsTab } from './panel/SettingsTab';
+import { SoundsTab } from './panel/SoundsTab';
+import { WelcomeTab } from './panel/WelcomeTab';
const TAURI = window.__TAURI__;
@@ -114,366 +114,3 @@ export function Footer({ context }) {
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
-
-function linkify(inputText) {
- var replacedText, replacePattern1, replacePattern2, replacePattern3;
-
- //URLs starting with http://, https://, or ftp://
- replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
- replacedText = inputText.replace(replacePattern1, '$1 ');
-
- //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
- replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
- replacedText = replacedText.replace(
- replacePattern2,
- '$1$2 ',
- );
-
- //Change email addresses to mailto:: links.
- replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
- replacedText = replacedText.replace(replacePattern3, '$1 ');
-
- return replacedText;
-}
-
-function WelcomeTab() {
- return (
-
-
- 🌀 welcome
-
-
- You have found strudel , a new live coding platform to write dynamic music
- pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
-
-
- 1. hit play - 2. change something -{' '}
- 3. hit update
-
- If you don't like what you hear, try shuffle !
-
-
- To learn more about what this all means, check out the{' '}
-
- interactive tutorial
-
- . Also feel free to join the{' '}
-
- tidalcycles discord channel
- {' '}
- to ask any questions, give feedback or just say hello.
-
-
about
-
- strudel is a JavaScript version of{' '}
-
- tidalcycles
-
- , which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
-
- github
-
- . Please consider to{' '}
-
- support this project
- {' '}
- to ensure ongoing development 💖
-
-
- );
-}
-
-function ConsoleTab({ log }) {
- return (
-
-
{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
-██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
-███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
-╚════██║ ██║ ██╔══██╗██║ ██║██║ ██║██╔══╝ ██║
-███████║ ██║ ██║ ██║╚██████╔╝██████╔╝███████╗███████╗
-╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝`}
- {log.map((l, i) => {
- const message = linkify(l.message);
- return (
-
-
- {l.count ? ` (${l.count})` : ''}
-
- );
- })}
-
- );
-}
-
-const getSamples = (samples) =>
- Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
-
-function SoundsTab() {
- const sounds = useStore(soundMap);
- const { soundsFilter } = useSettings();
- const soundEntries = useMemo(() => {
- let filtered = Object.entries(sounds).filter(([key]) => !key.startsWith('_'));
- if (!sounds) {
- return [];
- }
- if (soundsFilter === 'user') {
- return filtered.filter(([key, { data }]) => !data.prebake);
- }
- if (soundsFilter === 'drums') {
- return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
- }
- if (soundsFilter === 'samples') {
- return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
- }
- if (soundsFilter === 'synths') {
- return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
- }
- return filtered;
- }, [sounds, soundsFilter]);
- // holds mutable ref to current triggered sound
- const trigRef = useRef();
- // stop current sound on mouseup
- useEvent('mouseup', () => {
- const t = trigRef.current;
- trigRef.current = undefined;
- t?.then((ref) => {
- ref?.stop(getAudioContext().currentTime + 0.01);
- });
- });
- return (
-
-
- settingsMap.setKey('soundsFilter', value)}
- items={{
- samples: 'samples',
- drums: 'drum-machines',
- synths: 'Synths',
- user: 'User',
- }}
- >
-
-
- {soundEntries.map(([name, { data, onTrigger }]) => (
- {
- const ctx = getAudioContext();
- const params = {
- note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
- s: name,
- clip: 1,
- release: 0.5,
- };
- const time = ctx.currentTime + 0.05;
- const onended = () => trigRef.current?.node?.disconnect();
- trigRef.current = Promise.resolve(onTrigger(time, params, onended));
- trigRef.current.then((ref) => {
- ref?.node.connect(ctx.destination);
- });
- }}
- >
- {' '}
- {name}
- {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
- {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
-
- ))}
- {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
-
-
- );
-}
-
-function Checkbox({ label, value, onChange }) {
- return (
-
-
- {' ' + label}
-
- );
-}
-
-function ButtonGroup({ value, onChange, items }) {
- return (
-
- {Object.entries(items).map(([key, label], i, arr) => (
- onChange(key)}
- className={cx(
- 'px-2 border-b h-8',
- // i === 0 && 'rounded-l-md',
- // i === arr.length - 1 && 'rounded-r-md',
- // value === key ? 'bg-background' : 'bg-lineHighlight',
- value === key ? 'border-foreground' : 'border-transparent',
- )}
- >
- {label.toLowerCase()}
-
- ))}
-
- );
-}
-
-function SelectInput({ value, options, onChange }) {
- return (
- onChange(e.target.value)}
- >
- {Object.entries(options).map(([k, label]) => (
-
- {label}
-
- ))}
-
- );
-}
-
-function NumberSlider({ value, onChange, step = 1, ...rest }) {
- return (
-
- onChange(Number(e.target.value))}
- {...rest}
- />
- onChange(Number(e.target.value))}
- />
-
- );
-}
-
-function FormItem({ label, children }) {
- return (
-
- {label}
- {children}
-
- );
-}
-
-const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
-const fontFamilyOptions = {
- monospace: 'monospace',
- BigBlueTerminal: 'BigBlueTerminal',
- x3270: 'x3270',
- PressStart: 'PressStart2P',
- galactico: 'galactico',
- 'we-come-in-peace': 'we-come-in-peace',
- FiraCode: 'FiraCode',
- 'FiraCode-SemiBold': 'FiraCode SemiBold',
-};
-
-function SettingsTab({ scheduler }) {
- const {
- theme,
- keybindings,
- isLineNumbersDisplayed,
- isAutoCompletionEnabled,
- isLineWrappingEnabled,
- fontSize,
- fontFamily,
- panelPosition,
- } = useSettings();
-
- return (
-
- {/*
-
- {
- scheduler.setCps(scheduler.cps - 0.1);
- }}
- >
- slower
-
- {
- scheduler.setCps(scheduler.cps + 0.1);
- }}
- >
- faster
-
-
- */}
-
- settingsMap.setKey('theme', theme)} />
-
-
-
- settingsMap.setKey('fontFamily', fontFamily)}
- />
-
-
- settingsMap.setKey('fontSize', fontSize)}
- min={10}
- max={40}
- step={2}
- />
-
-
-
- settingsMap.setKey('keybindings', keybindings)}
- items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
- >
-
-
- settingsMap.setKey('panelPosition', value)}
- items={{ bottom: 'Bottom', right: 'Right' }}
- >
-
-
- settingsMap.setKey('isLineNumbersDisplayed', cbEvent.target.checked)}
- value={isLineNumbersDisplayed}
- />
- settingsMap.setKey('isAutoCompletionEnabled', cbEvent.target.checked)}
- value={isAutoCompletionEnabled}
- />
- settingsMap.setKey('isLineWrappingEnabled', cbEvent.target.checked)}
- value={isLineWrappingEnabled}
- />
-
-
Try clicking the logo in the top left!
-
- {
- if (confirm('Sure?')) {
- settingsMap.set(defaultSettings);
- }
- }}
- >
- restore default settings
-
-
-
- );
-}
diff --git a/website/src/repl/panel/ConsoleTab.jsx b/website/src/repl/panel/ConsoleTab.jsx
new file mode 100644
index 00000000..4472fd4c
--- /dev/null
+++ b/website/src/repl/panel/ConsoleTab.jsx
@@ -0,0 +1,45 @@
+import { cx } from '@strudel.cycles/react';
+import React from 'react';
+
+export function ConsoleTab({ log }) {
+ return (
+
+
{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
+██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
+███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
+╚════██║ ██║ ██╔══██╗██║ ██║██║ ██║██╔══╝ ██║
+███████║ ██║ ██║ ██║╚██████╔╝██████╔╝███████╗███████╗
+╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝`}
+ {log.map((l, i) => {
+ const message = linkify(l.message);
+ return (
+
+
+ {l.count ? ` (${l.count})` : ''}
+
+ );
+ })}
+
+ );
+}
+
+function linkify(inputText) {
+ var replacedText, replacePattern1, replacePattern2, replacePattern3;
+
+ //URLs starting with http://, https://, or ftp://
+ replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
+ replacedText = inputText.replace(replacePattern1, '$1 ');
+
+ //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
+ replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
+ replacedText = replacedText.replace(
+ replacePattern2,
+ '$1$2 ',
+ );
+
+ //Change email addresses to mailto:: links.
+ replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
+ replacedText = replacedText.replace(replacePattern3, '$1 ');
+
+ return replacedText;
+}
diff --git a/website/src/repl/FilesTab.jsx b/website/src/repl/panel/FilesTab.jsx
similarity index 97%
rename from website/src/repl/FilesTab.jsx
rename to website/src/repl/panel/FilesTab.jsx
index e04086b6..d78ec1ec 100644
--- a/website/src/repl/FilesTab.jsx
+++ b/website/src/repl/panel/FilesTab.jsx
@@ -1,6 +1,6 @@
import { Fragment, useEffect } from 'react';
import React, { useMemo, useState } from 'react';
-import { isAudioFile, readDir, dir, playFile } from './files.mjs';
+import { isAudioFile, readDir, dir, playFile } from '../files.mjs';
export function FilesTab() {
const [path, setPath] = useState([]);
diff --git a/website/src/repl/panel/Forms.jsx b/website/src/repl/panel/Forms.jsx
new file mode 100644
index 00000000..e45305e5
--- /dev/null
+++ b/website/src/repl/panel/Forms.jsx
@@ -0,0 +1,24 @@
+import { cx } from '@strudel.cycles/react';
+import React from 'react';
+
+export function ButtonGroup({ value, onChange, items }) {
+ return (
+
+ {Object.entries(items).map(([key, label], i, arr) => (
+ onChange(key)}
+ className={cx(
+ 'px-2 border-b h-8',
+ // i === 0 && 'rounded-l-md',
+ // i === arr.length - 1 && 'rounded-r-md',
+ // value === key ? 'bg-background' : 'bg-lineHighlight',
+ value === key ? 'border-foreground' : 'border-transparent',
+ )}
+ >
+ {label.toLowerCase()}
+
+ ))}
+
+ );
+}
diff --git a/website/src/repl/Reference.jsx b/website/src/repl/panel/Reference.jsx
similarity index 97%
rename from website/src/repl/Reference.jsx
rename to website/src/repl/panel/Reference.jsx
index de52982e..6c5d7d99 100644
--- a/website/src/repl/Reference.jsx
+++ b/website/src/repl/panel/Reference.jsx
@@ -1,4 +1,4 @@
-import jsdocJson from '../../../doc.json';
+import jsdocJson from '../../../../doc.json';
const visibleFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/panel/SettingsTab.jsx
new file mode 100644
index 00000000..5dea6258
--- /dev/null
+++ b/website/src/repl/panel/SettingsTab.jsx
@@ -0,0 +1,173 @@
+import React from 'react';
+import { defaultSettings, settingsMap, useSettings } from '../../settings.mjs';
+import { themes } from '../themes.mjs';
+import { ButtonGroup } from './Forms.jsx';
+
+function Checkbox({ label, value, onChange }) {
+ return (
+
+
+ {' ' + label}
+
+ );
+}
+
+function SelectInput({ value, options, onChange }) {
+ return (
+ onChange(e.target.value)}
+ >
+ {Object.entries(options).map(([k, label]) => (
+
+ {label}
+
+ ))}
+
+ );
+}
+
+function NumberSlider({ value, onChange, step = 1, ...rest }) {
+ return (
+
+ onChange(Number(e.target.value))}
+ {...rest}
+ />
+ onChange(Number(e.target.value))}
+ />
+
+ );
+}
+
+function FormItem({ label, children }) {
+ return (
+
+ {label}
+ {children}
+
+ );
+}
+
+const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
+const fontFamilyOptions = {
+ monospace: 'monospace',
+ BigBlueTerminal: 'BigBlueTerminal',
+ x3270: 'x3270',
+ PressStart: 'PressStart2P',
+ galactico: 'galactico',
+ 'we-come-in-peace': 'we-come-in-peace',
+ FiraCode: 'FiraCode',
+ 'FiraCode-SemiBold': 'FiraCode SemiBold',
+};
+
+export function SettingsTab() {
+ const {
+ theme,
+ keybindings,
+ isLineNumbersDisplayed,
+ isAutoCompletionEnabled,
+ isLineWrappingEnabled,
+ fontSize,
+ fontFamily,
+ panelPosition,
+ } = useSettings();
+
+ return (
+
+ {/*
+
+ {
+ scheduler.setCps(scheduler.cps - 0.1);
+ }}
+ >
+ slower
+
+ {
+ scheduler.setCps(scheduler.cps + 0.1);
+ }}
+ >
+ faster
+
+
+ */}
+
+ settingsMap.setKey('theme', theme)} />
+
+
+
+ settingsMap.setKey('fontFamily', fontFamily)}
+ />
+
+
+ settingsMap.setKey('fontSize', fontSize)}
+ min={10}
+ max={40}
+ step={2}
+ />
+
+
+
+ settingsMap.setKey('keybindings', keybindings)}
+ items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
+ >
+
+
+ settingsMap.setKey('panelPosition', value)}
+ items={{ bottom: 'Bottom', right: 'Right' }}
+ >
+
+
+ settingsMap.setKey('isLineNumbersDisplayed', cbEvent.target.checked)}
+ value={isLineNumbersDisplayed}
+ />
+ settingsMap.setKey('isAutoCompletionEnabled', cbEvent.target.checked)}
+ value={isAutoCompletionEnabled}
+ />
+ settingsMap.setKey('isLineWrappingEnabled', cbEvent.target.checked)}
+ value={isLineWrappingEnabled}
+ />
+
+
Try clicking the logo in the top left!
+
+ {
+ if (confirm('Sure?')) {
+ settingsMap.set(defaultSettings);
+ }
+ }}
+ >
+ restore default settings
+
+
+
+ );
+}
diff --git a/website/src/repl/panel/SoundsTab.jsx b/website/src/repl/panel/SoundsTab.jsx
new file mode 100644
index 00000000..b7fbb542
--- /dev/null
+++ b/website/src/repl/panel/SoundsTab.jsx
@@ -0,0 +1,89 @@
+import { useEvent } from '@strudel.cycles/react';
+// import { cx } from '@strudel.cycles/react';
+import { useStore } from '@nanostores/react';
+import { getAudioContext, soundMap } from '@strudel.cycles/webaudio';
+import React, { useMemo, useRef } from 'react';
+import { settingsMap, useSettings } from '../../settings.mjs';
+import { ButtonGroup } from './Forms.jsx';
+
+const getSamples = (samples) =>
+ Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
+
+export function SoundsTab() {
+ const sounds = useStore(soundMap);
+ const { soundsFilter } = useSettings();
+ const soundEntries = useMemo(() => {
+ let filtered = Object.entries(sounds).filter(([key]) => !key.startsWith('_'));
+ if (!sounds) {
+ return [];
+ }
+ if (soundsFilter === 'user') {
+ return filtered.filter(([key, { data }]) => !data.prebake);
+ }
+ if (soundsFilter === 'drums') {
+ return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
+ }
+ if (soundsFilter === 'samples') {
+ return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
+ }
+ if (soundsFilter === 'synths') {
+ return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
+ }
+ return filtered;
+ }, [sounds, soundsFilter]);
+ // holds mutable ref to current triggered sound
+ const trigRef = useRef();
+ // stop current sound on mouseup
+ useEvent('mouseup', () => {
+ const t = trigRef.current;
+ trigRef.current = undefined;
+ t?.then((ref) => {
+ ref?.stop(getAudioContext().currentTime + 0.01);
+ });
+ });
+ return (
+
+
+ settingsMap.setKey('soundsFilter', value)}
+ items={{
+ samples: 'samples',
+ drums: 'drum-machines',
+ synths: 'Synths',
+ user: 'User',
+ }}
+ >
+
+
+ {soundEntries.map(([name, { data, onTrigger }]) => (
+ {
+ const ctx = getAudioContext();
+ const params = {
+ note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
+ s: name,
+ clip: 1,
+ release: 0.5,
+ };
+ const time = ctx.currentTime + 0.05;
+ const onended = () => trigRef.current?.node?.disconnect();
+ trigRef.current = Promise.resolve(onTrigger(time, params, onended));
+ trigRef.current.then((ref) => {
+ ref?.node.connect(ctx.destination);
+ });
+ }}
+ >
+ {' '}
+ {name}
+ {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
+ {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
+
+ ))}
+ {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
+
+
+ );
+}
diff --git a/website/src/repl/panel/WelcomeTab.jsx b/website/src/repl/panel/WelcomeTab.jsx
new file mode 100644
index 00000000..aca5a813
--- /dev/null
+++ b/website/src/repl/panel/WelcomeTab.jsx
@@ -0,0 +1,49 @@
+import { cx } from '@strudel.cycles/react';
+import React from 'react';
+
+export function WelcomeTab() {
+ return (
+
+
+ 🌀 welcome
+
+
+ You have found strudel , a new live coding platform to write dynamic music
+ pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
+
+
+ 1. hit play - 2. change something -{' '}
+ 3. hit update
+
+ If you don't like what you hear, try shuffle !
+
+
+ To learn more about what this all means, check out the{' '}
+
+ interactive tutorial
+
+ . Also feel free to join the{' '}
+
+ tidalcycles discord channel
+ {' '}
+ to ask any questions, give feedback or just say hello.
+
+
about
+
+ strudel is a JavaScript version of{' '}
+
+ tidalcycles
+
+ , which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
+
+ github
+
+ . Please consider to{' '}
+
+ support this project
+ {' '}
+ to ensure ongoing development 💖
+
+
+ );
+}