memlnaut-nisps/playground/src/App.tsx
w1n5t0n f916344c9b feat(playground/modes): all 9 mode bindings + /modes route (meml-yt7)
Stream 9 (part 2/2) — concrete mode TSX components + routing.

Each firmware mode pairs the generated schema with the appropriate
primary input (XYPad for xy_pad schemas, VirtualJoystick for joystick
schemas) and feeds the runtime's processedOutputs into the shared
SliderBank/OutputDisplay/LossPlot layout. The set:

  - PAFSynthMode (xy_pad, voice spaces)
  - ChannelStripMode (joystick)
  - XIASRIMode (joystick — mic-input wiring deferred to stream 10)
  - VerbFXMode (joystick, voice spaces)
  - MEMLCeliumMode (xy_pad, voice spaces)
  - BreakOrMode (xy_pad)
  - ElysiamorfMode (xy_pad)
  - SoundAnalysisMIDIMode (audio_in fallback to joystick;
      mic capture is a stream-10 task — UI scaffold renders today)
  - C15Mode (browser-only placeholder; bridge not yet ported)

App.tsx adds a /modes route that renders <ModeSwitcher /> + the active
mode's component via Solid <Dynamic />. modes/index.ts exposes the
registry the switcher consumes.
2026-04-29 17:00:56 +03:00

145 lines
4.4 KiB
TypeScript

import { Component, createMemo, createSignal, lazy, onCleanup, Show } from 'solid-js';
import { Dynamic } from 'solid-js/web';
import { modeStore } from './stores/mode-store';
import { MODE_REGISTRY, getModeById } from './modes';
import { ModeSwitcher } from './modes/ModeSwitcher';
import styles from './App.module.css';
type Route = 'home' | 'primitives' | 'modes' | 'unknown';
function parseRoute(path: string): Route {
if (path === '' || path === '/' || path === '/index.html') return 'home';
if (path === '/dev/primitives' || path === '/dev/primitives/') return 'primitives';
if (path === '/modes' || path === '/modes/' || path.startsWith('/modes/')) return 'modes';
return 'unknown';
}
// Lazy-loaded so the home route doesn't pay the cost of loading every
// primitive demo. Stream 9/10 can grow the showcase freely.
const PrimitivesShowcase = lazy(() => import('./dev/PrimitivesShowcase'));
const App: Component = () => {
const [route, setRoute] = createSignal<Route>(parseRoute(window.location.pathname));
const onPop = () => setRoute(parseRoute(window.location.pathname));
window.addEventListener('popstate', onPop);
onCleanup(() => window.removeEventListener('popstate', onPop));
const navigate = (path: string) => {
if (window.location.pathname === path) return;
window.history.pushState({}, '', path);
setRoute(parseRoute(path));
};
return (
<div class={styles.app}>
<header class={styles.header}>
<strong>MEMLNaut</strong>
<span class={styles.dim}>playground</span>
<nav class={styles.nav}>
<button
type="button"
class={route() === 'home' ? styles.active : ''}
onClick={() => navigate('/')}
>
home
</button>
<button
type="button"
class={route() === 'modes' ? styles.active : ''}
onClick={() => navigate('/modes')}
>
/modes
</button>
<button
type="button"
class={route() === 'primitives' ? styles.active : ''}
onClick={() => navigate('/dev/primitives')}
>
/dev/primitives
</button>
</nav>
</header>
<main class={styles.main}>
<Show when={route() === 'home'}>
<Home navigate={navigate} />
</Show>
<Show when={route() === 'modes'}>
<ModesPage />
</Show>
<Show when={route() === 'primitives'}>
<PrimitivesShowcase />
</Show>
<Show when={route() === 'unknown'}>
<div class={styles.notFound}>
<h1>404</h1>
<p>No route at <code>{window.location.pathname}</code>.</p>
<p>
<a href="/" onClick={(e) => { e.preventDefault(); navigate('/'); }}>back to home</a>
</p>
</div>
</Show>
</main>
</div>
);
};
const ModesPage: Component = () => {
const activeId = () => modeStore.state.activeModeId ?? MODE_REGISTRY[0]!.id;
const ActiveComponent = createMemo(() => getModeById(activeId()).Component);
return (
<div>
<ModeSwitcher />
<Dynamic component={ActiveComponent()} />
</div>
);
};
interface HomeProps {
navigate: (path: string) => void;
}
const Home: Component<HomeProps> = (props) => {
return (
<div class={styles.home}>
<h1 class={styles.title}>MEMLNaut Playground</h1>
<p class={styles.tagline}>
Interactive ML control of audio. Stream 9: nine modes, one shell.
</p>
<ul class={styles.linkList}>
<li>
<a
href="/modes"
onClick={(e) => {
e.preventDefault();
props.navigate('/modes');
}}
>
Modes
</a>{' '}
pick a mode and start playing
</li>
<li>
<a
href="/dev/primitives"
onClick={(e) => {
e.preventDefault();
props.navigate('/dev/primitives');
}}
>
Primitives showcase
</a>{' '}
UI building blocks
</li>
</ul>
<p class={styles.note}>
ML inference runs on the main thread via WASM. Audio synthesis runs
in an AudioWorklet start it from inside any mode using the "Start
audio" button. Audio cannot autoplay (browser policy).
</p>
</div>
);
};
export default App;