feat: update button sizes and styles for improved UI consistency in Audiobook and button components
This commit is contained in:
@@ -409,7 +409,7 @@ function ProjectListSidebar({
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => onSelect(p.id)}
|
||||
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 ${
|
||||
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
|
||||
selectedId === p.id
|
||||
? 'bg-background border-border shadow-sm'
|
||||
: 'bg-background/50 border-border/40'
|
||||
@@ -541,7 +541,7 @@ function CharactersPanel({
|
||||
return (
|
||||
<button
|
||||
key={ch.id}
|
||||
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/60 border-b border-border/30 transition-colors"
|
||||
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/60 border-b border-border/30 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
||||
onClick={() => onScrollToChapter(ch.id)}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dotClass}`} />
|
||||
@@ -562,7 +562,7 @@ function CharactersPanel({
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{!isActive && status !== 'pending' && charCount > 0 && (
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs px-2 text-muted-foreground" onClick={onAnalyze} disabled={loadingAction}>
|
||||
<Button size="xs" variant="ghost" className="text-muted-foreground" onClick={onAnalyze} disabled={loadingAction}>
|
||||
{t('projectCard.reanalyze')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -758,6 +758,7 @@ function ChaptersPanel({
|
||||
const [regeneratingSegs, setRegeneratingSegs] = useState<Set<number>>(new Set())
|
||||
const [audioVersions, setAudioVersions] = useState<Record<number, number>>({})
|
||||
const prevSegStatusRef = useRef<Record<number, string>>({})
|
||||
const initialExpandDoneRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollToChapterId) return
|
||||
@@ -823,12 +824,23 @@ function ChaptersPanel({
|
||||
const generatingChapterIds = detail.chapters
|
||||
.filter(ch => segments.some(s => s.chapter_index === ch.chapter_index && s.status === 'generating'))
|
||||
.map(ch => ch.id)
|
||||
if (generatingChapterIds.length === 0) return
|
||||
setExpandedChapters(prev => {
|
||||
const next = new Set(prev)
|
||||
generatingChapterIds.forEach(id => next.add(id))
|
||||
return next.size === prev.size ? prev : next
|
||||
})
|
||||
if (generatingChapterIds.length > 0) {
|
||||
setExpandedChapters(prev => {
|
||||
const next = new Set(prev)
|
||||
generatingChapterIds.forEach(id => next.add(id))
|
||||
return next.size === prev.size ? prev : next
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!initialExpandDoneRef.current) {
|
||||
initialExpandDoneRef.current = true
|
||||
const chapterIdsWithSegs = detail.chapters
|
||||
.filter(ch => segments.some(s => s.chapter_index === ch.chapter_index))
|
||||
.map(ch => ch.id)
|
||||
if (chapterIdsWithSegs.length > 0) {
|
||||
setExpandedChapters(new Set(chapterIdsWithSegs))
|
||||
}
|
||||
}
|
||||
}, [segments, detail])
|
||||
|
||||
const hasChapters = detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status)
|
||||
@@ -842,17 +854,17 @@ function ChaptersPanel({
|
||||
{hasChapters && (
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && (
|
||||
<Button size="sm" variant="outline" className="h-6 text-xs px-2" disabled={loadingAction} onClick={onParseAll}>
|
||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={onParseAll}>
|
||||
{t('projectCard.chapters.parseAll')}
|
||||
</Button>
|
||||
)}
|
||||
{detail!.chapters.some(c => c.status === 'ready') && (
|
||||
<Button size="sm" variant="outline" className="h-6 text-xs px-2" disabled={loadingAction} onClick={onGenerateAll}>
|
||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={onGenerateAll}>
|
||||
{t('projectCard.chapters.generateAll')}
|
||||
</Button>
|
||||
)}
|
||||
{detail!.chapters.some(c => ['pending', 'error'].includes(c.status)) && detail!.chapters.some(c => c.status === 'ready') && (
|
||||
<Button size="sm" className="h-6 text-xs px-2" disabled={loadingAction} onClick={onProcessAll}>
|
||||
<Button size="xs" disabled={loadingAction} onClick={onProcessAll}>
|
||||
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : t('projectCard.chapters.processAll')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -882,7 +894,7 @@ function ChaptersPanel({
|
||||
<div key={ch.id} id={`ch-${ch.id}`}>
|
||||
{/* Chapter header — flat, full-width, click to expand */}
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 bg-muted/40 hover:bg-muted/70 border-b text-left transition-colors"
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 bg-muted/40 hover:bg-muted/70 border-b text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
||||
onClick={toggleChExpand}
|
||||
>
|
||||
<span className="shrink-0 text-muted-foreground">
|
||||
@@ -891,7 +903,7 @@ function ChaptersPanel({
|
||||
<span className="text-xs font-medium flex-1 truncate">{chTitle}</span>
|
||||
<span className="shrink-0 flex items-center gap-1.5" onClick={e => e.stopPropagation()}>
|
||||
{ch.status === 'pending' && (
|
||||
<Button size="sm" variant="outline" className="h-5 text-[11px] px-2" onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||
<Button size="xs" variant="outline" onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||
{t('projectCard.chapters.parse')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -902,13 +914,13 @@ function ChaptersPanel({
|
||||
)}
|
||||
{ch.status === 'ready' && !chGenerating && !chAllDone && !generatingChapterIndices.has(ch.chapter_index) && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" className="h-5 text-[11px] px-2" disabled={loadingAction} onClick={() => {
|
||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={() => {
|
||||
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
|
||||
onGenerate(ch.chapter_index)
|
||||
}}>
|
||||
{t('projectCard.chapters.generate')}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[11px] px-1.5 text-muted-foreground" disabled={loadingAction} onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||
{t('projectCard.chapters.reparse')}
|
||||
</Button>
|
||||
</>
|
||||
@@ -918,22 +930,22 @@ function ChaptersPanel({
|
||||
<Loader2 className="h-3 w-3 animate-spin" />{t('projectCard.chapters.segmentProgress', { done: chDone, total: chTotal })}
|
||||
</span>
|
||||
)}
|
||||
{ch.status === 'ready' && chAllDone && (
|
||||
{(ch.status === 'done' || (ch.status === 'ready' && chAllDone)) && (
|
||||
<>
|
||||
<span className="text-[11px] text-muted-foreground">{t('projectCard.chapters.doneBadge', { count: chDone })}</span>
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[11px] px-1.5 text-muted-foreground" disabled={loadingAction} onClick={() => {
|
||||
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => {
|
||||
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
|
||||
onGenerate(ch.chapter_index, true)
|
||||
}}>
|
||||
<RefreshCw className="h-3 w-3 mr-1" />{t('projectCard.chapters.generate')}
|
||||
<RefreshCw className="h-3 w-3" />{t('projectCard.chapters.generate')}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-5 w-5 p-0" onClick={() => onDownload(ch.chapter_index)} title={t('projectCard.downloadAll')}>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => onDownload(ch.chapter_index)} title={t('projectCard.downloadAll')}>
|
||||
<Download className="h-3 w-3" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{ch.status === 'error' && (
|
||||
<Button size="sm" variant="outline" className="h-5 text-[11px] px-2 text-destructive border-destructive/40" onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||
<Button size="xs" variant="outline" className="text-destructive border-destructive/40" onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||
{t('projectCard.chapters.reparse')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -979,19 +991,19 @@ function ChaptersPanel({
|
||||
<div className="ml-auto flex items-center gap-0.5 shrink-0">
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<Button size="icon" variant="ghost" className="h-5 w-5" onClick={() => startEdit(seg)} disabled={isRegenerating} title={t('projectCard.segments.edit')}>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => startEdit(seg)} disabled={isRegenerating} title={t('projectCard.segments.edit')}>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" className="h-5 w-5" onClick={() => handleRegenerate(seg.id)} disabled={isRegenerating} title={t('projectCard.segments.regenerate')}>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => handleRegenerate(seg.id)} disabled={isRegenerating} title={t('projectCard.segments.regenerate')}>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="icon" variant="ghost" className="h-5 w-5 text-destructive" onClick={cancelEdit} title={t('projectCard.segments.cancel')}>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-destructive" onClick={cancelEdit} title={t('projectCard.segments.cancel')}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" className="h-5 w-5 text-primary" onClick={() => saveEdit(seg.id)} disabled={isSaving} title={t('projectCard.segments.save')}>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-primary" onClick={() => saveEdit(seg.id)} disabled={isSaving} title={t('projectCard.segments.save')}>
|
||||
{isSaving ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
|
||||
</Button>
|
||||
</>
|
||||
@@ -1484,22 +1496,22 @@ export default function Audiobook() {
|
||||
{t(`status.${displayStatus}`, { defaultValue: displayStatus })}
|
||||
</Badge>
|
||||
{status === 'pending' && (
|
||||
<Button size="sm" className="h-7 text-xs px-2" onClick={handleAnalyze} disabled={loadingAction}>
|
||||
<Button size="sm" onClick={handleAnalyze} disabled={loadingAction}>
|
||||
{t('projectCard.analyze')}
|
||||
</Button>
|
||||
)}
|
||||
{status === 'ready' && (
|
||||
<Button size="sm" className="h-7 text-xs px-2" onClick={handleProcessAll} disabled={loadingAction}>
|
||||
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
|
||||
<Button size="sm" onClick={handleProcessAll} disabled={loadingAction}>
|
||||
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : null}
|
||||
{t('projectCard.processAll')}
|
||||
</Button>
|
||||
)}
|
||||
{status === 'done' && (
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs px-2" onClick={() => handleDownload()} disabled={loadingAction}>
|
||||
<Download className="h-3 w-3 mr-1" />{t('projectCard.downloadAll')}
|
||||
<Button size="sm" variant="outline" onClick={() => handleDownload()} disabled={loadingAction}>
|
||||
<Download className="h-3 w-3" />{t('projectCard.downloadAll')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7 shrink-0" onClick={handleDelete}>
|
||||
<Button size="icon" variant="ghost" className="h-8 w-8 shrink-0" onClick={handleDelete}>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1537,14 +1549,14 @@ export default function Audiobook() {
|
||||
{chaptersError > 0 && (
|
||||
<>
|
||||
<span className="text-destructive">({t('projectCard.chaptersError', { count: chaptersError })})</span>
|
||||
<Button size="sm" variant="outline" className="h-5 text-[10px] px-1.5 text-destructive border-destructive/40" onClick={handleRetryFailed}>
|
||||
<Button size="xs" variant="outline" className="text-destructive border-destructive/40" onClick={handleRetryFailed}>
|
||||
{t('projectCard.retryFailed')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{chaptersParsing > 0 && totalCount > 0 && (
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}>
|
||||
<Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
|
||||
{t('projectCard.cancelParsing')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -1560,7 +1572,7 @@ export default function Audiobook() {
|
||||
<span>{t('projectCard.segmentsProgress', { done: doneCount, total: totalCount })}</span>
|
||||
</div>
|
||||
{!chaptersParsing && hasGenerating && (
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}>
|
||||
<Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
|
||||
{t('projectCard.cancelGenerating')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -1570,7 +1582,7 @@ export default function Audiobook() {
|
||||
)}
|
||||
{chaptersParsing > 0 && !totalCount && (
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}>
|
||||
<Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
|
||||
{t('projectCard.cancelParsing')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1591,6 +1603,7 @@ export default function Audiobook() {
|
||||
onScrollToChapter={(id) => setScrollToChapterId(id)}
|
||||
/>
|
||||
<ChaptersPanel
|
||||
key={selectedProject.id}
|
||||
project={selectedProject}
|
||||
detail={detail}
|
||||
segments={segments}
|
||||
|
||||
Reference in New Issue
Block a user