Skip to content

Commit 796ef13

Browse files
feat: make generation bar and layout fully responsive (#32)
1 parent 4cad1d9 commit 796ef13

File tree

5 files changed

+73
-12
lines changed

5 files changed

+73
-12
lines changed

client/src/components/generation/generation-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export function GenerationInput({ voiceId, generate, isGenerating, capabilities,
171171
<div className="space-y-1.5 border-b px-3 py-2">
172172
{/* Speed + Tone */}
173173
<div className="flex flex-wrap items-center gap-2">
174-
<ToggleGroup type="single" size="sm" variant="outline" value={activeSpeed} onValueChange={applySpeed}>
174+
<ToggleGroup type="single" size="sm" variant="outline" value={activeSpeed} onValueChange={applySpeed} className="flex-wrap">
175175
{SPEED_PRESETS.map(({ key, rate, labelKey }) => (
176176
<ToggleGroupItem key={key} value={key} className="gap-1 text-xs">
177177
<span className="size-1.5 shrink-0 rounded-full" style={{ background: getSpeedBorderColor(rate) }} />
@@ -183,7 +183,7 @@ export function GenerationInput({ voiceId, generate, isGenerating, capabilities,
183183
{capabilities.tone && (
184184
<>
185185
<div className="h-4 w-px shrink-0 bg-border" />
186-
<ToggleGroup type="single" size="sm" variant="outline" value={activeTone} onValueChange={applyTone}>
186+
<ToggleGroup type="single" size="sm" variant="outline" value={activeTone} onValueChange={applyTone} className="flex-wrap">
187187
{TONE_PRESETS.map(({ key, labelKey }) => (
188188
<ToggleGroupItem key={key} value={key} className="text-xs">
189189
{t(labelKey)}

client/src/components/layout/app-layout.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import type { ReactNode } from 'react';
22
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
3+
import { useIsDesktop, useIsMobile } from '@/hooks/use-mobile';
34
import { AppSidebar } from './app-sidebar';
5+
import { BottomNav } from './bottom-nav';
46
import { GenerationBar } from './generation-bar';
57

68
export function AppLayout({ children }: { children: ReactNode }) {
9+
const isMobile = useIsMobile();
10+
const isDesktop = useIsDesktop();
11+
712
return (
8-
<SidebarProvider className="!h-svh">
9-
<AppSidebar />
13+
<SidebarProvider className="!h-svh" open={!isMobile && isDesktop} onOpenChange={() => {}}>
14+
{!isMobile && <AppSidebar />}
1015
<SidebarInset className="relative overflow-hidden">
1116
<main className="absolute inset-0 overflow-x-hidden overflow-y-auto">
12-
<div className="p-6 pb-48">{children}</div>
17+
<div className={`p-6 ${isMobile ? 'pb-64' : 'pb-48'}`}>{children}</div>
1318
<div className="sticky bottom-0 -mt-16 h-16 bg-gradient-to-t from-background to-transparent pointer-events-none" />
1419
</main>
15-
<div className="absolute bottom-0 left-0 right-0 z-10 pointer-events-none">
20+
<div className={`absolute left-0 right-0 z-10 pointer-events-none ${isMobile ? 'bottom-14' : 'bottom-0'}`}>
1621
<div className="pointer-events-auto">
1722
<GenerationBar />
1823
</div>
1924
</div>
25+
{isMobile && (
26+
<div className="absolute bottom-0 left-0 right-0 z-20">
27+
<BottomNav />
28+
</div>
29+
)}
2030
</SidebarInset>
2131
</SidebarProvider>
2232
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Link, useRouterState } from '@tanstack/react-router';
2+
import { AudioLines, Box, Clock, LayoutDashboard, Settings } from 'lucide-react';
3+
import { useTranslation } from 'react-i18next';
4+
import { cn } from '@/lib/utils';
5+
6+
export function BottomNav() {
7+
const { t } = useTranslation();
8+
const router = useRouterState();
9+
const currentPath = router.location.pathname;
10+
11+
const navItems = [
12+
{ label: t('nav.voices'), href: '/voices', icon: AudioLines, primary: false },
13+
{ label: t('nav.history'), href: '/history', icon: Clock, primary: false },
14+
{ label: t('nav.dashboard'), href: '/', icon: LayoutDashboard, primary: true },
15+
{ label: t('nav.models'), href: '/models', icon: Box, primary: false },
16+
{ label: t('nav.settings'), href: '/settings', icon: Settings, primary: false },
17+
];
18+
19+
return (
20+
<div className="flex h-14 items-center justify-around border-t bg-background px-2">
21+
{navItems.map((item) => (
22+
<Link
23+
key={item.href}
24+
to={item.href}
25+
className={cn(
26+
'flex flex-col items-center gap-0.5 rounded-md px-3 py-1.5 text-muted-foreground transition-colors hover:text-foreground',
27+
currentPath === item.href && 'text-foreground',
28+
)}
29+
>
30+
<item.icon className={item.primary ? 'size-7' : 'size-5'} />
31+
<span className="text-[10px]">{item.label}</span>
32+
</Link>
33+
))}
34+
</div>
35+
);
36+
}

client/src/components/layout/generation-bar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ export function GenerationBar() {
127127
const capabilities = { tone: selectedModel?.supportsInstruct ?? false, effects: selectedModel?.supportsEffects ?? false };
128128

129129
return (
130-
<div className="px-16 pb-16 pt-2 flex justify-center">
131-
<div className="w-full max-w-6xl">
132-
<div className="rounded-xl shadow-2xl ring-2 ring-border/60 bg-card">
130+
<div className="flex justify-center md:px-8 md:pb-10 md:pt-2 lg:px-16 lg:pb-16">
131+
<div className="w-full md:max-w-6xl">
132+
<div className="rounded-none md:rounded-xl shadow-2xl ring-2 ring-border/60 bg-card">
133133
<div className={`transition-opacity${isGenerating ? ' pointer-events-none opacity-50' : ''}`}>
134134
<GenerationInput voiceId={voiceId} generate={generate} isGenerating={isGenerating} capabilities={capabilities} voiceSelector={<VoiceCombobox voiceId={voiceId} onChange={setVoiceId} />} />
135135
</div>

client/src/hooks/use-mobile.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
import * as React from 'react';
22

33
const MOBILE_BREAKPOINT = 768;
4+
const DESKTOP_BREAKPOINT = 1280;
45

56
export function useIsMobile() {
6-
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
7+
const [isMobile, setIsMobile] = React.useState(() => window.innerWidth < MOBILE_BREAKPOINT);
78

89
React.useEffect(() => {
910
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
1011
const onChange = () => {
1112
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
1213
};
1314
mql.addEventListener('change', onChange);
14-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
1515
return () => mql.removeEventListener('change', onChange);
1616
}, []);
1717

18-
return !!isMobile;
18+
return isMobile;
19+
}
20+
21+
export function useIsDesktop() {
22+
const [isDesktop, setIsDesktop] = React.useState(() => window.innerWidth >= DESKTOP_BREAKPOINT);
23+
24+
React.useEffect(() => {
25+
const mql = window.matchMedia(`(min-width: ${DESKTOP_BREAKPOINT}px)`);
26+
const onChange = () => {
27+
setIsDesktop(window.innerWidth >= DESKTOP_BREAKPOINT);
28+
};
29+
mql.addEventListener('change', onChange);
30+
return () => mql.removeEventListener('change', onChange);
31+
}, []);
32+
33+
return isDesktop;
1934
}

0 commit comments

Comments
 (0)