mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 04:40:40 -07:00
responsive layout fixes, version in sidebar, fixed voice card height, hide player title at small widths
This commit is contained in:
@@ -7,8 +7,8 @@ import { Slider } from '@/components/ui/slider';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { formatAudioDuration } from '@/lib/utils/audio';
|
||||
import { debug } from '@/lib/utils/debug';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { usePlatform } from '@/platform/PlatformContext';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
|
||||
export function AudioPlayer() {
|
||||
const platform = usePlatform();
|
||||
@@ -360,7 +360,7 @@ export function AudioPlayer() {
|
||||
if (shouldAutoPlayNow) {
|
||||
// Clear the flag first
|
||||
usePlayerStore.getState().clearAutoPlayFlag();
|
||||
|
||||
|
||||
// Use a small delay to ensure audio element is fully ready
|
||||
setTimeout(() => {
|
||||
wavesurfer.play().catch((error) => {
|
||||
@@ -665,7 +665,7 @@ export function AudioPlayer() {
|
||||
// Handle shouldAutoPlay flag - for story mode auto-advance
|
||||
const shouldAutoPlay = usePlayerStore((state) => state.shouldAutoPlay);
|
||||
const clearAutoPlayFlag = usePlayerStore((state) => state.clearAutoPlayFlag);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const wavesurfer = wavesurferRef.current;
|
||||
if (!wavesurfer || !shouldAutoPlay || duration === 0) {
|
||||
@@ -833,11 +833,7 @@ export function AudioPlayer() {
|
||||
className="shrink-0"
|
||||
title={duration === 0 && !isLoading ? 'Audio not loaded' : ''}
|
||||
aria-label={
|
||||
duration === 0 && !isLoading
|
||||
? 'Audio not loaded'
|
||||
: isPlaying
|
||||
? 'Pause'
|
||||
: 'Play'
|
||||
duration === 0 && !isLoading ? 'Audio not loaded' : isPlaying ? 'Pause' : 'Play'
|
||||
}
|
||||
>
|
||||
{isPlaying ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
|
||||
@@ -872,7 +868,9 @@ export function AudioPlayer() {
|
||||
|
||||
{/* Title */}
|
||||
{title && (
|
||||
<div className="text-sm font-medium truncate max-w-[200px] shrink-0">{title}</div>
|
||||
<div className="text-sm font-medium truncate max-w-[200px] shrink-0 hidden lg:block">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loop Button */}
|
||||
@@ -888,7 +886,11 @@ export function AudioPlayer() {
|
||||
</Button>
|
||||
|
||||
{/* Volume Control */}
|
||||
<div className="flex items-center gap-2 shrink-0 w-[120px]" role="group" aria-label="Volume">
|
||||
<div
|
||||
className="flex items-center gap-2 shrink-0 w-[120px]"
|
||||
role="group"
|
||||
aria-label="Volume"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
@@ -165,7 +165,7 @@ export function FloatingGenerateBox({
|
||||
isStoriesRoute
|
||||
? // Position aligned with story list: after sidebar + padding, width 360px
|
||||
'left-[calc(5rem+2rem)] w-[360px]'
|
||||
: 'left-[calc(5rem+2rem)] w-[calc((100%-5rem-4rem)/2-1rem)]',
|
||||
: 'left-[calc(5rem+2rem)] right-8 lg:right-auto lg:w-[calc((100%-5rem-4rem)/2-1rem)]',
|
||||
)}
|
||||
style={{
|
||||
// On stories route: offset by track editor height when visible
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
|
||||
import { useImportProfile } from '@/lib/hooks/useProfiles';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
@@ -77,9 +77,9 @@ export function MainEditor() {
|
||||
|
||||
return (
|
||||
// Main view: Profiles top left, Generator bottom left, History right
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full min-h-0 overflow-hidden relative">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 lg:gap-6 h-full min-h-0 overflow-hidden relative">
|
||||
{/* Left Column */}
|
||||
<div className="flex flex-col min-h-0 overflow-hidden relative">
|
||||
<div className="flex flex-col min-h-0 overflow-hidden relative lg:overflow-hidden">
|
||||
{/* Scroll Mask - Always visible, behind content */}
|
||||
<div className="absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-background to-transparent z-0 pointer-events-none" />
|
||||
|
||||
@@ -110,10 +110,7 @@ export function MainEditor() {
|
||||
{/* Scrollable Content */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={cn(
|
||||
'flex-1 min-h-0 overflow-y-auto pt-14',
|
||||
isPlayerVisible ? BOTTOM_SAFE_AREA_PADDING : 'pb-4',
|
||||
)}
|
||||
className={cn('flex-1 min-h-0 overflow-y-auto pt-14 pb-4', isPlayerVisible && 'lg:pb-32')}
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="shrink-0 flex flex-col">
|
||||
@@ -123,6 +120,9 @@ export function MainEditor() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider - single column only */}
|
||||
{/* <div className="border-t border-border -my-3 lg:hidden" /> */}
|
||||
|
||||
{/* Right Column - History */}
|
||||
<div className="flex flex-col min-h-0 overflow-hidden">
|
||||
<HistoryTable />
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Link, useMatchRoute } from '@tanstack/react-router';
|
||||
import { BookOpen, Box, Mic, Server, Speaker, Volume2 } from 'lucide-react';
|
||||
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { version } from '../../package.json';
|
||||
|
||||
interface SidebarProps {
|
||||
isMacOS?: boolean;
|
||||
@@ -18,6 +20,7 @@ const tabs = [
|
||||
|
||||
export function Sidebar({ isMacOS }: SidebarProps) {
|
||||
const matchRoute = useMatchRoute();
|
||||
const isPlayerOpen = !!usePlayerStore((s) => s.audioUrl);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -56,6 +59,14 @@ export function Sidebar({ isMacOS }: SidebarProps) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Version */}
|
||||
<div
|
||||
className="mt-auto text-[10px] text-muted-foreground/50 transition-all duration-300"
|
||||
style={{ paddingBottom: isPlayerOpen ? '7rem' : undefined }}
|
||||
>
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
||||
<>
|
||||
<Card
|
||||
className={cn(
|
||||
'cursor-pointer hover:shadow-md transition-all flex flex-col',
|
||||
'cursor-pointer hover:shadow-md transition-all flex flex-col h-[162px]',
|
||||
isSelected && 'ring-2 ring-primary shadow-md',
|
||||
)}
|
||||
onClick={handleSelect}
|
||||
|
||||
@@ -41,9 +41,11 @@ export function ProfileList() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 grid-cols-3 auto-rows-auto p-1 pb-[150px]">
|
||||
<div className="flex gap-4 overflow-x-auto p-1 pb-1 lg:grid lg:grid-cols-3 lg:auto-rows-auto lg:overflow-x-visible lg:pb-[150px]">
|
||||
{allProfiles.map((profile) => (
|
||||
<ProfileCard key={profile.id} profile={profile} />
|
||||
<div key={profile.id} className="shrink-0 w-[200px] lg:w-auto lg:shrink">
|
||||
<ProfileCard profile={profile} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -90,12 +90,12 @@ export function useGenerationProgress() {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Generation complete!',
|
||||
description: data.duration
|
||||
? `Audio generated (${data.duration.toFixed(2)}s)`
|
||||
: 'Audio generated',
|
||||
});
|
||||
// toast({
|
||||
// title: 'Generation complete!',
|
||||
// description: data.duration
|
||||
// ? `Audio generated (${data.duration.toFixed(2)}s)`
|
||||
// : 'Audio generated',
|
||||
// });
|
||||
}
|
||||
|
||||
// Auto-play if enabled and nothing is currently playing
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user