Refactor UI components for improved layout consistency and responsiveness

- Updated App and AudioTab components to utilize TOP_SAFE_AREA_PADDING and BOTTOM_SAFE_AREA_PADDING constants for better layout adjustments.
- Enhanced HistoryTable and ModelManagement components for improved visual consistency.
- Streamlined import statements and component structure across various files for better organization and readability.
This commit is contained in:
Jamie Pine
2026-01-27 17:12:16 -08:00
parent a3cbe7f2b6
commit a42a946586
5 changed files with 112 additions and 112 deletions
+8 -3
View File
@@ -11,6 +11,7 @@ import { Sidebar } from '@/components/Sidebar';
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster';
import { VoicesTab } from '@/components/VoicesTab/VoicesTab'; import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks'; import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
import { import {
@@ -20,9 +21,8 @@ import {
setupWindowCloseHandler, setupWindowCloseHandler,
startServer, startServer,
} from '@/lib/tauri'; } from '@/lib/tauri';
import { useServerStore } from '@/stores/serverStore';
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import { useServerStore } from '@/stores/serverStore';
// Track if server is starting to prevent duplicate starts // Track if server is starting to prevent duplicate starts
let serverStarting = false; let serverStarting = false;
@@ -140,7 +140,12 @@ function App() {
// Show loading screen while server is starting in Tauri // Show loading screen while server is starting in Tauri
if (isTauri() && !serverReady) { if (isTauri() && !serverReady) {
return ( return (
<div className={cn('min-h-screen bg-background flex items-center justify-center', TOP_SAFE_AREA_PADDING)}> <div
className={cn(
'min-h-screen bg-background flex items-center justify-center',
TOP_SAFE_AREA_PADDING,
)}
>
<TitleBarDragRegion /> <TitleBarDragRegion />
<div className="text-center space-y-6"> <div className="text-center space-y-6">
<div className="flex justify-center relative"> <div className="flex justify-center relative">
+44 -43
View File
@@ -1,5 +1,8 @@
import { Edit, Plus, Trash2, Speaker, CheckCircle2, Check } from 'lucide-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { Check, CheckCircle2, Edit, Plus, Speaker, Trash2 } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@@ -18,14 +21,11 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api/client'; import { apiClient } from '@/lib/api/client';
import { isTauri } from '@/lib/tauri';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from '@/stores/playerStore';
import { cn } from '@/lib/utils/cn';
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui'; import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
import { isTauri } from '@/lib/tauri';
import { cn } from '@/lib/utils/cn';
import { usePlayerStore } from '@/stores/playerStore';
interface AudioDevice { interface AudioDevice {
id: string; id: string;
@@ -69,8 +69,7 @@ export function AudioTab() {
}); });
const createChannel = useMutation({ const createChannel = useMutation({
mutationFn: (data: { name: string; device_ids: string[] }) => mutationFn: (data: { name: string; device_ids: string[] }) => apiClient.createChannel(data),
apiClient.createChannel(data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['channels'] }); queryClient.invalidateQueries({ queryKey: ['channels'] });
setCreateDialogOpen(false); setCreateDialogOpen(false);
@@ -144,12 +143,18 @@ export function AudioTab() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full min-h-0"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full min-h-0">
{/* Left Column - Channels */} {/* Left Column - Channels */}
<div className={cn('flex flex-col min-h-0 overflow-y-auto', isPlayerVisible && BOTTOM_SAFE_AREA_PADDING)}> <div
className={cn(
'flex flex-col min-h-0 overflow-y-auto',
isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,
)}
>
{allChannels.length === 0 ? ( {allChannels.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md"> <div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md">
<Speaker className="h-12 w-12 text-muted-foreground mb-4" /> <Speaker className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
No audio channels yet. Create your first channel to route voices to specific devices. No audio channels yet. Create your first channel to route voices to specific
devices.
</p> </p>
<Button onClick={() => setCreateDialogOpen(true)}> <Button onClick={() => setCreateDialogOpen(true)}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
@@ -187,8 +192,8 @@ export function AudioTab() {
Output Devices Output Devices
</div> </div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{channel.device_ids.length > 0 ? ( {channel.device_ids.length > 0
channel.device_ids.map((deviceId) => { ? channel.device_ids.map((deviceId) => {
const device = allDevices.find((d) => d.id === deviceId); const device = allDevices.find((d) => d.id === deviceId);
return ( return (
<Badge <Badge
@@ -200,16 +205,14 @@ export function AudioTab() {
</Badge> </Badge>
); );
}) })
) : ( : (() => {
(() => {
const defaultDevice = allDevices.find((d) => d.is_default); const defaultDevice = allDevices.find((d) => d.is_default);
return defaultDevice ? ( return defaultDevice ? (
<Badge variant="outline" className="text-xs font-normal"> <Badge variant="outline" className="text-xs font-normal">
{defaultDevice.name} {defaultDevice.name}
</Badge> </Badge>
) : null; ) : null;
})() })()}
)}
</div> </div>
</div> </div>
@@ -259,7 +262,12 @@ export function AudioTab() {
</div> </div>
{/* Right Column - Available Devices */} {/* Right Column - Available Devices */}
<div className={cn('flex flex-col min-h-0 overflow-y-auto', isPlayerVisible && BOTTOM_SAFE_AREA_PADDING)}> <div
className={cn(
'flex flex-col min-h-0 overflow-y-auto',
isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,
)}
>
<div className="shrink-0 mb-4"> <div className="shrink-0 mb-4">
<h3 className="text-lg font-semibold">Available Devices</h3> <h3 className="text-lg font-semibold">Available Devices</h3>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
@@ -279,7 +287,8 @@ export function AudioTab() {
(selectedChannel.device_ids.length === 0 (selectedChannel.device_ids.length === 0
? device.is_default ? device.is_default
: selectedChannel.device_ids.includes(device.id)); : selectedChannel.device_ids.includes(device.id));
const canToggle = selectedChannelId && selectedChannel && !selectedChannel.is_default; const canToggle =
selectedChannelId && selectedChannel && !selectedChannel.is_default;
const handleDeviceClick = () => { const handleDeviceClick = () => {
if (!canToggle || !selectedChannel) return; if (!canToggle || !selectedChannel) return;
@@ -314,9 +323,7 @@ export function AudioTab() {
<div <div
className={cn( className={cn(
'h-4 w-4 rounded border-2 flex items-center justify-center shrink-0', 'h-4 w-4 rounded border-2 flex items-center justify-center shrink-0',
isConnected isConnected ? 'bg-accent border-accent' : 'border-muted-foreground/30',
? 'bg-accent border-accent'
: 'border-muted-foreground/30',
)} )}
> >
{isConnected && <Check className="h-3 w-3 text-accent-foreground" />} {isConnected && <Check className="h-3 w-3 text-accent-foreground" />}
@@ -335,9 +342,7 @@ export function AudioTab() {
<div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md"> <div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md">
<CheckCircle2 className="h-12 w-12 text-muted-foreground mb-4" /> <CheckCircle2 className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground text-center"> <p className="text-muted-foreground text-center">
{isTauri() {isTauri() ? 'No audio devices found' : 'Audio device selection requires Tauri'}
? 'No audio devices found'
: 'Audio device selection requires Tauri'}
</p> </p>
</div> </div>
)} )}
@@ -355,7 +360,8 @@ export function AudioTab() {
/> />
{/* Edit Channel Dialog */} {/* Edit Channel Dialog */}
{editingChannel && (() => { {editingChannel &&
(() => {
const channel = channels?.find((c) => c.id === editingChannel); const channel = channels?.find((c) => c.id === editingChannel);
return channel ? ( return channel ? (
<EditChannelDialog <EditChannelDialog
@@ -396,9 +402,7 @@ function ChannelVoicesList({ channelId }: { channelId: string }) {
}); });
const voiceNames = const voiceNames =
voices?.profile_ids voices?.profile_ids.map((id) => profiles?.find((p) => p.id === id)?.name).filter(Boolean) || [];
.map((id) => profiles?.find((p) => p.id === id)?.name)
.filter(Boolean) || [];
return ( return (
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
@@ -422,12 +426,7 @@ interface CreateChannelDialogProps {
onCreate: (name: string, deviceIds: string[]) => void; onCreate: (name: string, deviceIds: string[]) => void;
} }
function CreateChannelDialog({ function CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateChannelDialogProps) {
open,
onOpenChange,
devices,
onCreate,
}: CreateChannelDialogProps) {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [selectedDevices, setSelectedDevices] = useState<string[]>([]); const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
@@ -492,7 +491,9 @@ function CreateChannelDialog({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))} onClick={() =>
setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))
}
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
@@ -562,11 +563,7 @@ function EditChannelDialog({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label htmlFor="edit-channel-name">Channel Name</Label> <Label htmlFor="edit-channel-name">Channel Name</Label>
<Input <Input id="edit-channel-name" value={name} onChange={(e) => setName(e.target.value)} />
id="edit-channel-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div> </div>
<div> <div>
<Label>Output Devices</Label> <Label>Output Devices</Label>
@@ -602,7 +599,9 @@ function EditChannelDialog({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))} onClick={() =>
setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))
}
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
@@ -646,7 +645,9 @@ function EditChannelDialog({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setSelectedVoices(selectedVoices.filter((id) => id !== profileId))} onClick={() =>
setSelectedVoices(selectedVoices.filter((id) => id !== profileId))
}
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
+1 -1
View File
@@ -17,6 +17,7 @@ import {
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { apiClient } from '@/lib/api/client'; import { apiClient } from '@/lib/api/client';
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
import { import {
useDeleteGeneration, useDeleteGeneration,
useExportGeneration, useExportGeneration,
@@ -27,7 +28,6 @@ import {
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import { formatDate, formatDuration } from '@/lib/utils/format'; import { formatDate, formatDuration } from '@/lib/utils/format';
import { usePlayerStore } from '@/stores/playerStore'; import { usePlayerStore } from '@/stores/playerStore';
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
// OLD TABLE-BASED COMPONENT - REMOVED (can be found in git history) // OLD TABLE-BASED COMPONENT - REMOVED (can be found in git history)
// This is the new alternate history view with fixed height rows // This is the new alternate history view with fixed height rows
@@ -1,13 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { CheckCircle2, Download, Loader2, Trash2 } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { apiClient } from '@/lib/api/client';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Loader2, Download, CheckCircle2, Trash2 } from 'lucide-react';
import { ModelProgress } from './ModelProgress';
import { useToast } from '@/components/ui/use-toast';
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -18,6 +11,13 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useToast } from '@/components/ui/use-toast';
import { apiClient } from '@/lib/api/client';
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
import { ModelProgress } from './ModelProgress';
export function ModelManagement() { export function ModelManagement() {
const { toast } = useToast(); const { toast } = useToast();
@@ -197,8 +197,8 @@ export function ModelManagement() {
{modelToDelete?.sizeMb && ( {modelToDelete?.sizeMb && (
<> <>
{' '} {' '}
This will free up {formatSize(modelToDelete.sizeMb)} of disk space. The model This will free up {formatSize(modelToDelete.sizeMb)} of disk space. The model will
will need to be re-downloaded if you want to use it again. need to be re-downloaded if you want to use it again.
</> </>
)} )}
</AlertDialogDescription> </AlertDialogDescription>
@@ -244,13 +244,7 @@ interface ModelItemProps {
formatSize: (sizeMb?: number) => string; formatSize: (sizeMb?: number) => string;
} }
function ModelItem({ function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: ModelItemProps) {
model,
onDownload,
onDelete,
isDownloading,
formatSize,
}: ModelItemProps) {
return ( return (
<div className="flex items-center justify-between p-3 border rounded-lg"> <div className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex-1"> <div className="flex-1">
+3 -3
View File
@@ -17,15 +17,15 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { ProfileForm } from '@/components/VoiceProfiles/ProfileForm';
import { apiClient } from '@/lib/api/client'; import { apiClient } from '@/lib/api/client';
import type { VoiceProfileResponse } from '@/lib/api/types'; import type { VoiceProfileResponse } from '@/lib/api/types';
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
import { useHistory } from '@/lib/hooks/useHistory'; import { useHistory } from '@/lib/hooks/useHistory';
import { useDeleteProfile, useProfileSamples, useProfiles } from '@/lib/hooks/useProfiles'; import { useDeleteProfile, useProfileSamples, useProfiles } from '@/lib/hooks/useProfiles';
import { useUIStore } from '@/stores/uiStore';
import { ProfileForm } from '@/components/VoiceProfiles/ProfileForm';
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import { usePlayerStore } from '@/stores/playerStore'; import { usePlayerStore } from '@/stores/playerStore';
import { useUIStore } from '@/stores/uiStore';
export function VoicesTab() { export function VoicesTab() {
const { data: profiles, isLoading } = useProfiles(); const { data: profiles, isLoading } = useProfiles();