diff --git a/session_state.db b/session_state.db index 4d5a5df..90ad56b 100644 Binary files a/session_state.db and b/session_state.db differ diff --git a/src/App.tsx b/src/App.tsx index 77e4b03..91ed936 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { useWebSocket } from './hooks/useWebSocket'; import { useDrumMachine } from './hooks/useDrumMachine'; import { useSession } from './hooks/useSession'; import { Sequencer } from './components/Sequencer'; -import { ShareIcon, CursorIcon } from './components/icons'; +import { ShareIcon, CursorIcon, UploadIcon, DownloadIcon } from './components/icons'; const App: React.FC = () => { const sessionId = useSession(); @@ -29,32 +29,66 @@ const App: React.FC = () => { }; return ( -
-
-
-
-
- - {isConnected ? 'Connected' : 'Disconnected'} +
+
+
+
+ {/* Logo area */} +
+

AG Beats

+

+ Craft your beats and bass lines with this interactive step sequencer. +

-
- - {localCopyMessage && ( -
- Link copied! -
- )} + + {/* Status and Actions area */} +
+
+ + + +
+ +
+ + {isConnected ? 'Connected' : 'Disconnected'} +
+
+ + {/* Link copied message */} + {localCopyMessage && ( +
+ Link copied! +
+ )} +
- -

AG Beats

-

Craft your beats and bass lines with this interactive step sequencer.

diff --git a/src/components/Sequencer.tsx b/src/components/Sequencer.tsx index a37ad77..ecb0a31 100644 --- a/src/components/Sequencer.tsx +++ b/src/components/Sequencer.tsx @@ -37,7 +37,20 @@ const ControlButton: React.FC<{ onClick?: () => void; children: React.ReactNode; title={title} className={`flex items-center justify-center gap-2 px-3 py-1.5 text-[11px] font-black uppercase tracking-widest text-slate-600 bg-white border border-slate-200 rounded shadow-sm hover:bg-slate-50 hover:border-slate-300 transition-all active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed ${className}`} > - {children} + {React.Children.map(children, child => { + if (typeof child === 'string') { + return {child}; + } + if (React.isValidElement(child) && child.type === React.Fragment) { + return React.Children.map((child.props as any).children, subChild => { + if (typeof subChild === 'string') { + return {subChild}; + } + return subChild; + }); + } + return child; + })} ); @@ -67,6 +80,15 @@ export const Sequencer: React.FC = ({ const [isBassDragging, setIsBassDragging] = useState(false); const [bassDragActivationState, setBassDragActivationState] = useState(false); const [isClearModalOpen, setIsClearModalOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const mql = window.matchMedia('(max-width: 767px)'); + setIsMobile(mql.matches); + const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches); + mql.addEventListener('change', handler); + return () => mql.removeEventListener('change', handler); + }, []); useEffect(() => { const handleMouseUp = () => { @@ -129,7 +151,7 @@ export const Sequencer: React.FC = ({ }; return ( -
+
setIsClearModalOpen(false)} @@ -140,28 +162,28 @@ export const Sequencer: React.FC = ({ {/* Toolbar */} -
-
- +
+
+ {isPlaying ? <> Stop : <> Play} - setIsClearModalOpen(true)}> + setIsClearModalOpen(true)} className="flex-1 lg:flex-none"> Clear
-
-
- Tempo -
+
+
+ Tempo +
{tempo} BPM = MAX_TEMPO}>
-
- Steps -
+
+ Steps +
{steps} = MAX_STEPS}> @@ -169,39 +191,51 @@ export const Sequencer: React.FC = ({
-
- fileInputRef.current?.click()}> +
+ fileInputRef.current?.click()} className="flex-1 lg:flex-none"> Import - + Export
{/* Drum Machine Section */} -
-
-
+
+
+ Drums +
handleDrumVolumeChange(Number(e.target.value))} - className="absolute inset-0 opacity-0 cursor-pointer [writing-mode:vertical-lr] [direction:rtl]" + className="absolute inset-0 opacity-0 cursor-pointer z-30" + style={isMobile ? { width: '100%', height: '100%' } : { width: '100%', height: '100%', writingMode: 'vertical-lr', direction: 'rtl' }} /> -
-
+
+
- Drums
-
-
+
+
{/* Header helper */}
{Array.from({ length: steps }, (_, i) => ( @@ -212,8 +246,8 @@ export const Sequencer: React.FC = ({ {INSTRUMENTS.map((instrument, instIndex) => ( -
- +
+ {instrument.name}
@@ -226,7 +260,7 @@ export const Sequencer: React.FC = ({ key={`${instrument.name}-${stepIndex}`} onMouseDown={() => handleCellMouseDown(instIndex, stepIndex)} onMouseEnter={() => handleCellMouseEnter(instIndex, stepIndex)} - className={`h-9 rounded-sm cursor-pointer transition-all duration-75 relative group overflow-hidden + className={`h-8 md:h-9 rounded-sm cursor-pointer transition-all duration-75 relative group overflow-hidden ${isActive ? 'bg-orange-500 shadow-md shadow-orange-200' : 'bg-slate-100 hover:bg-slate-200'} ${isCurrent ? 'ring-2 ring-blue-400 ring-offset-1 z-10' : ''} ${isFourth && stepIndex !== 0 ? 'border-l-2 border-slate-200/50' : ''} @@ -243,27 +277,39 @@ export const Sequencer: React.FC = ({
{/* Bass Section */} -
-
-
+
+
+ Bass +
handleBassVolumeChange(Number(e.target.value))} - className="absolute inset-0 opacity-0 cursor-pointer [writing-mode:vertical-lr] [direction:rtl]" + className="absolute inset-0 opacity-0 cursor-pointer z-30" + style={isMobile ? { width: '100%', height: '100%' } : { width: '100%', height: '100%', writingMode: 'vertical-lr', direction: 'rtl' }} /> -
-
+
+
- Bass
-
-
+
+
{/* Header helper */}
{Array.from({ length: steps }, (_, i) => ( @@ -274,8 +320,8 @@ export const Sequencer: React.FC = ({ {[...BASS_NOTES].reverse().map((note) => ( -
- +
+ {note.name}