refactor(ui): replace loading text with skeleton placeholders across panels

Update multiple UI panels to use skeleton components instead of loading text or spinners.
Standardize loading states in AudiobookExportModal, AdminFeaturesPanel, AdminProvidersPanel,
DocumentSelectionModal, and SegmentsSidebar for improved visual consistency and user feedback.
This commit is contained in:
Richard R 2026-05-13 23:17:47 -06:00
parent d5eebd3b11
commit 89391410da
5 changed files with 173 additions and 12 deletions

View file

@ -7,7 +7,6 @@ import { ProgressPopup } from '@/components/ProgressPopup';
import { ProgressCard } from '@/components/ProgressCard';
import { DownloadIcon, CheckCircleIcon, XCircleIcon, ClockIcon, ChevronUpDownIcon, RefreshIcon, DotsVerticalIcon } from '@/components/icons/Icons';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { LoadingSpinner } from '@/components/Spinner';
import { useConfig } from '@/contexts/ConfigContext';
import { useTTS } from '@/contexts/TTSContext';
import { VoicesControlBase } from '@/components/player/VoicesControlBase';
@ -472,9 +471,7 @@ export function AudiobookExportModal({
subtitle="Only leaving the document cancels generation."
>
{isLoadingExisting ? (
<div className="flex justify-center py-8">
<LoadingSpinner />
</div>
<AudiobookSettingsSkeleton />
) : (
<>
<div className="space-y-4">
@ -937,3 +934,50 @@ export function AudiobookExportModal({
</>
);
}
function AudiobookSettingsSkeleton() {
return (
<div className="space-y-4 animate-pulse" aria-label="Loading audiobook settings" aria-busy="true">
<div className="w-full rounded-xl border border-offbase bg-background overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-offbase bg-base">
<div className="h-4 w-40 rounded bg-offbase" />
<div className="h-5 w-14 rounded bg-offbase" />
</div>
<div className="p-4 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<div className="h-3 w-16 rounded bg-offbase" />
<div className="h-9 w-full rounded-md bg-offbase" />
</div>
<div className="space-y-1.5">
<div className="h-3 w-16 rounded bg-offbase" />
<div className="h-9 w-full rounded-md bg-offbase" />
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<div className="h-3 w-24 rounded bg-offbase" />
<div className="h-2 w-full rounded bg-offbase" />
</div>
<div className="space-y-1.5">
<div className="h-3 w-20 rounded bg-offbase" />
<div className="h-2 w-full rounded bg-offbase" />
</div>
</div>
<div className="h-9 w-full rounded-md bg-offbase" />
</div>
</div>
<div className="w-full rounded-xl border border-offbase bg-background overflow-hidden">
<div className="px-4 py-3 border-b border-offbase bg-base">
<div className="h-4 w-28 rounded bg-offbase" />
</div>
<div className="p-4 space-y-2">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="h-16 rounded-lg border border-offbase bg-base" />
))}
</div>
</div>
</div>
);
}

View file

@ -172,9 +172,7 @@ export function AdminFeaturesPanel() {
if (!data) {
return (
<Section title="TTS defaults" subtitle="Loading…">
<p className="text-xs text-muted">Fetching current values</p>
</Section>
<AdminFeaturesSkeleton />
);
}
@ -340,6 +338,46 @@ export function AdminFeaturesPanel() {
);
}
function AdminFeaturesSkeleton() {
return (
<div className="space-y-4 animate-pulse" aria-label="Loading feature settings" aria-busy="true">
<Section
title="TTS defaults"
subtitle="Defaults for new users."
action={<div className="h-4 w-16 rounded bg-offbase" />}
>
<div className="space-y-1.5 pb-2 border-b border-offbase">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<div className="h-4 w-40 rounded bg-offbase" />
<div className="h-3 w-56 rounded bg-offbase" />
</div>
<div className="h-5 w-20 rounded bg-offbase" />
</div>
<div className="h-9 w-full rounded-md bg-offbase" />
</div>
<div className="space-y-2">
<div className="h-14 w-full rounded-md border border-offbase bg-background" />
<div className="h-14 w-full rounded-md border border-offbase bg-background" />
<div className="h-14 w-full rounded-md border border-offbase bg-background" />
</div>
</Section>
<Section
title="Site features"
subtitle="Feature flags for all users."
action={<div className="h-4 w-24 rounded bg-offbase" />}
>
<div className="space-y-2">
<div className="h-14 w-full rounded-md border border-offbase bg-background" />
<div className="h-14 w-full rounded-md border border-offbase bg-background" />
<div className="h-14 w-full rounded-md border border-offbase bg-background" />
</div>
</Section>
</div>
);
}
function SourceBadge({
source,
dirty,

View file

@ -502,7 +502,7 @@ export function AdminProvidersPanel() {
<div className="space-y-0">
{isLoading ? (
<p className="text-xs text-muted">Loading</p>
<ProvidersListSkeleton />
) : providers.length === 0 ? (
<p className="text-xs text-muted py-2">No shared providers configured yet.</p>
) : (
@ -547,3 +547,28 @@ export function AdminProvidersPanel() {
</Section>
);
}
function ProvidersListSkeleton() {
const rows = Array.from({ length: 4 });
return (
<div className="animate-pulse space-y-0.5" aria-label="Loading shared providers" aria-busy="true">
{rows.map((_, index) => (
<div key={index} className="py-1.5 border-b border-offbase last:border-b-0">
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0 space-y-1.5">
<div className="flex items-center gap-2">
<div className="h-4 w-36 rounded bg-offbase" />
<div className="h-4 w-20 rounded bg-offbase" />
</div>
<div className="h-3 w-4/5 rounded bg-offbase" />
</div>
<div className="shrink-0 flex gap-1.5 pt-0.5">
<div className="h-7 w-14 rounded-md bg-offbase" />
<div className="h-7 w-16 rounded-md bg-offbase" />
</div>
</div>
</div>
))}
</div>
);
}

View file

@ -164,7 +164,7 @@ export function DocumentSelectionModal({
<div className="flex-1 overflow-auto border border-offbase rounded-lg bg-background p-2 min-h-0">
{isLoading ? (
<div className="flex items-center justify-center h-full text-muted">Loading documents...</div>
<DocumentSelectionSkeleton />
) : errorMessage ? (
<div className="flex items-center justify-center h-full text-red-500">{errorMessage}</div>
) : files.length === 0 ? (
@ -229,3 +229,18 @@ export function DocumentSelectionModal({
</Transition>
);
}
function DocumentSelectionSkeleton() {
const rows = Array.from({ length: 9 });
return (
<div className="h-full animate-pulse space-y-0.5" aria-label="Loading documents" aria-busy="true">
{rows.map((_, index) => (
<div key={index} className="flex items-center gap-3 px-3 py-2 rounded-md">
<div className="h-4 w-4 rounded-sm bg-offbase border border-offbase" />
<div className="h-3.5 flex-1 rounded bg-offbase" />
<div className="h-3 w-14 rounded bg-offbase" />
</div>
))}
</div>
);
}

View file

@ -631,7 +631,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
) : null}
</>
) : isManifestLoading ? (
<span>Loading</span>
<div className="animate-pulse h-3 w-36 rounded bg-offbase" aria-label="Loading segment summary" aria-busy="true" />
) : hasManifestError ? (
<span className="text-red-500">error</span>
) : (
@ -645,7 +645,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
<div className="px-4 py-6 text-sm text-red-500">{manifestErrorMessage}</div>
)}
{isManifestLoading && (
<div className="px-4 py-6 text-sm text-muted">Loading segments</div>
<SegmentsListSkeleton />
)}
{hasLoadedManifest && rowsToRender.length === 0 && (
<div className="px-4 py-10 flex flex-col items-center text-center gap-2">
@ -787,13 +787,52 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
</ul>
)}
{hasLoadedManifest && isLoadingMoreManifest && (
<div className="px-4 py-3 text-xs text-muted">Loading more segments</div>
<SegmentsListSkeletonRows />
)}
</div>
</ReaderSidebarShell>
);
}
function SegmentsListSkeleton() {
return (
<div className="px-4 py-3">
<div className="animate-pulse space-y-3" aria-label="Loading segments" aria-busy="true">
<div className="h-3 w-40 rounded bg-offbase" />
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="rounded-md border border-offbase bg-base px-3 py-2.5">
<div className="flex items-start gap-3">
<div className="h-3.5 w-8 rounded bg-offbase mt-0.5 shrink-0" />
<div className="min-w-0 flex-1 space-y-2">
<div className="h-3.5 w-11/12 rounded bg-offbase" />
<div className="h-3.5 w-3/4 rounded bg-offbase" />
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-offbase" />
<div className="h-3 w-12 rounded bg-offbase" />
<div className="h-4 w-20 rounded bg-offbase" />
</div>
</div>
<div className="h-6 w-6 rounded bg-offbase shrink-0" />
</div>
</div>
))}
</div>
</div>
);
}
function SegmentsListSkeletonRows() {
return (
<div className="px-4 py-3 animate-pulse" aria-label="Loading more segments" aria-busy="true">
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="h-12 rounded-md border border-offbase bg-base" />
))}
</div>
</div>
);
}
function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
return (
<Popover className="relative shrink-0">