refactor: restructure to monorepo with npm workspaces (Phase 0)

Move frontend to packages/client/, server to packages/server/.
Root package.json uses npm workspaces to orchestrate both.

Structure:
  reaktor/
    packages/client/  (React + Vite + Tone.js frontend)
    packages/server/  (static file server, future API)
    dist/             (built output, shared)
    docker-compose.yml (app + PostgreSQL for future backend)

- npm run dev → runs Vite dev server from client workspace
- npm run build → builds client, outputs to root dist/
- npm run start → runs server.js serving dist/
- Dockerfile updated for multi-stage monorepo build
- docker-compose.yml added with PostgreSQL service (ready for Phase 1)
- All imports and paths preserved, zero functionality change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 19:52:57 +01:00
parent 4baa86eed0
commit b058997889
59 changed files with 96 additions and 33 deletions

View File

@@ -0,0 +1,112 @@
import React, { useRef, useEffect, useState } from 'react';
import { getAnalyserData } from '../engine/audioEngine.js';
// Zoom levels: how many samples to display (from a 2048-sample buffer)
// Fewer samples = zoomed in (more detail), more samples = zoomed out (more time visible)
const ZOOM_LEVELS = [64, 128, 256, 512, 1024, 2048];
const DEFAULT_ZOOM = 2; // index → 256 samples
export default function ScopeDisplay({ moduleId }) {
const canvasRef = useRef(null);
const rafRef = useRef(null);
const [zoomIdx, setZoomIdx] = useState(DEFAULT_ZOOM);
const zoomRef = useRef(ZOOM_LEVELS[DEFAULT_ZOOM]);
// Keep ref in sync so the draw loop picks it up without re-creating the effect
useEffect(() => { zoomRef.current = ZOOM_LEVELS[zoomIdx]; }, [zoomIdx]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const w = canvas.width = 160;
const h = canvas.height = 60;
let frameCount = 0;
const draw = () => {
frameCount++;
rafRef.current = requestAnimationFrame(draw);
// Throttle to ~30fps to reduce main thread pressure during playback
if (frameCount % 2 !== 0) return;
ctx.fillStyle = '#050510';
ctx.fillRect(0, 0, w, h);
// Grid lines
ctx.strokeStyle = '#151530';
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2);
ctx.moveTo(0, h / 4); ctx.lineTo(w, h / 4);
ctx.moveTo(0, h * 3 / 4); ctx.lineTo(w, h * 3 / 4);
for (let x = w / 4; x < w; x += w / 4) {
ctx.moveTo(x, 0); ctx.lineTo(x, h);
}
ctx.stroke();
const data = getAnalyserData(moduleId);
if (data && data.length > 0) {
const samplesToShow = zoomRef.current;
// Center the window in the buffer
const offset = Math.max(0, Math.floor((data.length - samplesToShow) / 2));
const end = Math.min(data.length, offset + samplesToShow);
ctx.strokeStyle = '#00e5ff';
ctx.lineWidth = 1.5;
ctx.beginPath();
const count = end - offset;
const step = w / count;
for (let i = 0; i < count; i++) {
const y = h / 2 + data[offset + i] * h / 2 * -1;
if (i === 0) ctx.moveTo(0, y);
else ctx.lineTo(i * step, y);
}
ctx.stroke();
}
};
draw();
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
}, [moduleId]);
const canZoomIn = zoomIdx > 0;
const canZoomOut = zoomIdx < ZOOM_LEVELS.length - 1;
return (
<div style={{ position: 'relative' }}>
<canvas ref={canvasRef} className="scope-canvas" />
<div style={{
position: 'absolute', bottom: 2, right: 2,
display: 'flex', gap: 2,
}}>
<button
onClick={() => canZoomOut && setZoomIdx(i => i + 1)}
disabled={!canZoomOut}
title="Zoom out (más tiempo)"
style={{
width: 18, height: 18, padding: 0,
background: canZoomOut ? '#1a1a3a' : '#0a0a15',
border: '1px solid #333', borderRadius: 3,
color: canZoomOut ? '#00e5ff' : '#333',
cursor: canZoomOut ? 'pointer' : 'default',
fontSize: 12, lineHeight: '16px', fontWeight: 'bold',
}}
></button>
<button
onClick={() => canZoomIn && setZoomIdx(i => i - 1)}
disabled={!canZoomIn}
title="Zoom in (más detalle)"
style={{
width: 18, height: 18, padding: 0,
background: canZoomIn ? '#1a1a3a' : '#0a0a15',
border: '1px solid #333', borderRadius: 3,
color: canZoomIn ? '#00e5ff' : '#333',
cursor: canZoomIn ? 'pointer' : 'default',
fontSize: 12, lineHeight: '16px', fontWeight: 'bold',
}}
>+</button>
</div>
</div>
);
}