Enhance UI components and styles for improved user experience

- Added global styles to hide scrollbars across all browsers for a cleaner interface.
- Refactored HistoryTable component to highlight currently playing audio with conditional styling.
- Implemented a confirmation dialog for deleting voice profiles in ProfileCard, enhancing user feedback and preventing accidental deletions.
- Updated ProfileList to rename the section from 'Voice Profiles' to 'Voicebox' for better branding and clarity.
This commit is contained in:
Jamie Pine
2026-01-25 14:44:47 -08:00
parent 98cfe2dc5a
commit 0e38d47855
4 changed files with 62 additions and 15 deletions
+10 -5
View File
@@ -10,6 +10,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { cn } from '@/lib/utils/cn';
import { apiClient } from '@/lib/api/client'; import { apiClient } from '@/lib/api/client';
import { useDeleteGeneration, useHistory } from '@/lib/hooks/useHistory'; import { useDeleteGeneration, useHistory } from '@/lib/hooks/useHistory';
import { formatDate, formatDuration } from '@/lib/utils/format'; import { formatDate, formatDuration } from '@/lib/utils/format';
@@ -60,8 +61,6 @@ export function HistoryTable() {
return ( return (
<div className="flex flex-col h-full min-h-0"> <div className="flex flex-col h-full min-h-0">
<h2 className="text-2xl font-bold mb-4 shrink-0">Generation History</h2>
{history.length === 0 ? ( {history.length === 0 ? (
<div className="text-center py-12 text-muted-foreground flex-1 flex items-center justify-center"> <div className="text-center py-12 text-muted-foreground flex-1 flex items-center justify-center">
No generation history yet. Generate your first audio to see it here. No generation history yet. Generate your first audio to see it here.
@@ -81,8 +80,13 @@ export function HistoryTable() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{history.map((gen) => ( {history.map((gen) => {
<TableRow key={gen.id}> const isCurrentlyPlaying = currentAudioId === gen.id && isPlaying;
return (
<TableRow
key={gen.id}
className={cn(isCurrentlyPlaying && 'bg-muted/50')}
>
<TableCell className="max-w-[200px] truncate">{gen.text}</TableCell> <TableCell className="max-w-[200px] truncate">{gen.text}</TableCell>
<TableCell>{gen.profile_name}</TableCell> <TableCell>{gen.profile_name}</TableCell>
<TableCell> <TableCell>
@@ -123,7 +127,8 @@ export function HistoryTable() {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} );
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
@@ -1,7 +1,16 @@
import { Edit, Eye, Mic, Trash2 } from 'lucide-react'; import { Edit, Eye, Mic, Trash2 } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { CircleButton } from '@/components/ui/circle-button'; import { CircleButton } from '@/components/ui/circle-button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import type { VoiceProfileResponse } from '@/lib/api/types'; import type { VoiceProfileResponse } from '@/lib/api/types';
@@ -16,6 +25,7 @@ interface ProfileCardProps {
export function ProfileCard({ profile }: ProfileCardProps) { export function ProfileCard({ profile }: ProfileCardProps) {
const [detailOpen, setDetailOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const deleteProfile = useDeleteProfile(); const deleteProfile = useDeleteProfile();
const setEditingProfileId = useUIStore((state) => state.setEditingProfileId); const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);
const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen); const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen);
@@ -33,12 +43,14 @@ export function ProfileCard({ profile }: ProfileCardProps) {
setProfileDialogOpen(true); setProfileDialogOpen(true);
}; };
const handleDelete = () => { const handleDeleteClick = (e: React.MouseEvent) => {
if ( e.stopPropagation();
confirm(`Are you sure you want to delete "${profile.name}"? This action cannot be undone.`) setDeleteDialogOpen(true);
) { };
const handleDeleteConfirm = () => {
deleteProfile.mutate(profile.id); deleteProfile.mutate(profile.id);
} setDeleteDialogOpen(false);
}; };
return ( return (
@@ -67,12 +79,15 @@ export function ProfileCard({ profile }: ProfileCardProps) {
/> />
<CircleButton <CircleButton
icon={Edit} icon={Edit}
onClick={handleEdit} onClick={(e) => {
e.stopPropagation();
handleEdit();
}}
aria-label="Edit profile" aria-label="Edit profile"
/> />
<CircleButton <CircleButton
icon={Trash2} icon={Trash2}
onClick={handleDelete} onClick={handleDeleteClick}
disabled={deleteProfile.isPending} disabled={deleteProfile.isPending}
aria-label="Delete profile" aria-label="Delete profile"
/> />
@@ -93,6 +108,25 @@ export function ProfileCard({ profile }: ProfileCardProps) {
</Card> </Card>
<ProfileDetail profileId={profile.id} open={detailOpen} onOpenChange={setDetailOpen} /> <ProfileDetail profileId={profile.id} open={detailOpen} onOpenChange={setDetailOpen} />
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Profile</DialogTitle>
<DialogDescription>
Are you sure you want to delete "{profile.name}"? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={handleDeleteConfirm} disabled={deleteProfile.isPending}>
{deleteProfile.isPending ? 'Deleting...' : 'Delete'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</> </>
); );
} }
@@ -31,7 +31,7 @@ export function ProfileList() {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center justify-between mb-4 shrink-0"> <div className="flex items-center justify-between mb-4 shrink-0">
<h2 className="text-2xl font-bold">Voice Profiles</h2> <h2 className="text-2xl font-bold">Voicebox</h2>
<Button onClick={() => setDialogOpen(true)}> <Button onClick={() => setDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
New Profile New Profile
@@ -53,7 +53,7 @@ export function ProfileList() {
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<div className="grid gap-4 grid-cols-3 auto-rows-fr"> <div className="grid gap-4 grid-cols-3 auto-rows-fr p-1">
{allProfiles.map((profile) => ( {allProfiles.map((profile) => (
<ProfileCard key={profile.id} profile={profile} /> <ProfileCard key={profile.id} profile={profile} />
))} ))}
+8
View File
@@ -109,6 +109,14 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
/* Hide scrollbars globally */
* {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
*::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
} }
@layer utilities { @layer utilities {