skeleton loading

This commit is contained in:
EthanHealy01 2025-08-12 13:38:21 +01:00
parent 8581829f6e
commit 53e9dbb66e
7 changed files with 66 additions and 49 deletions

View File

@ -109,10 +109,12 @@ export default function ToolPanel() {
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Tool content */} {/* Tool content */}
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
{selectedToolKey && (
<ToolRenderer <ToolRenderer
selectedToolKey={selectedToolKey} selectedToolKey={selectedToolKey}
onPreviewFile={setPreviewFile} onPreviewFile={setPreviewFile}
/> />
)}
</div> </div>
</div> </div>
)} )}

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext, useMemo, useRef } from 'react'; import React, { createContext, useContext, useMemo, useRef, useEffect, useState } from 'react';
import { Paper, Text, Stack, Box, Flex } from '@mantine/core'; import { Paper, Text, Stack, Box, Flex } from '@mantine/core';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
@ -22,6 +22,10 @@ export interface ToolStepProps {
completedMessage?: string; completedMessage?: string;
helpText?: string; helpText?: string;
showNumber?: boolean; showNumber?: boolean;
/** Show a brief skeleton for smoother tool transitions */
enableInitialSkeleton?: boolean;
/** Duration for the initial skeleton in milliseconds */
initialSkeletonMs?: number;
tooltip?: { tooltip?: {
content?: React.ReactNode; content?: React.ReactNode;
tips?: TooltipTip[]; tips?: TooltipTip[];
@ -74,6 +78,8 @@ const ToolStep = ({
completedMessage, completedMessage,
helpText, helpText,
showNumber, showNumber,
enableInitialSkeleton = true,
initialSkeletonMs = 800,
tooltip tooltip
}: ToolStepProps) => { }: ToolStepProps) => {
if (!isVisible) return null; if (!isVisible) return null;
@ -88,6 +94,16 @@ const ToolStep = ({
const stepNumber = parent?.getStepNumber?.() || 1; const stepNumber = parent?.getStepNumber?.() || 1;
// Show a step-sized skeleton immediately after mount to indicate tool switch
const [showInitialSkeleton, setShowInitialSkeleton] = useState(enableInitialSkeleton);
useEffect(() => {
if (!enableInitialSkeleton) return;
setShowInitialSkeleton(true);
const id = setTimeout(() => setShowInitialSkeleton(false), initialSkeletonMs);
return () => clearTimeout(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<Paper <Paper
p="md" p="md"
@ -109,11 +125,19 @@ const ToolStep = ({
> >
<Flex align="center" gap="sm"> <Flex align="center" gap="sm">
{shouldShowNumber && ( {shouldShowNumber && (
showInitialSkeleton ? (
<Box w={18} h={18} bg="gray.1" style={{ borderRadius: 4 }} />
) : (
<Text fw={500} size="lg" c="dimmed"> <Text fw={500} size="lg" c="dimmed">
{stepNumber} {stepNumber}
</Text> </Text>
)
)}
{showInitialSkeleton ? (
<Box w={80} h={20} bg="gray.1" style={{ borderRadius: 4 }} />
) : (
renderTooltipTitle(title, tooltip, isCollapsed)
)} )}
{renderTooltipTitle(title, tooltip, isCollapsed)}
</Flex> </Flex>
{isCollapsed ? ( {isCollapsed ? (
@ -133,7 +157,10 @@ const ToolStep = ({
{isCollapsed ? ( {isCollapsed ? (
<Box> <Box>
{isCompleted && completedMessage && ( {showInitialSkeleton ? (
<Box w="40%" h={14} bg="gray.1" style={{ borderRadius: 4 }} />
) : (
isCompleted && completedMessage && (
<Text size="sm" c="green"> <Text size="sm" c="green">
{completedMessage} {completedMessage}
{onCollapsedClick && ( {onCollapsedClick && (
@ -142,6 +169,7 @@ const ToolStep = ({
</Text> </Text>
)} )}
</Text> </Text>
)
)} )}
</Box> </Box>
) : ( ) : (
@ -151,7 +179,16 @@ const ToolStep = ({
{helpText} {helpText}
</Text> </Text>
)} )}
{children} {showInitialSkeleton ? (
<>
<Box w="80%" h={16} bg="gray.1" style={{ borderRadius: 4 }} />
<Box w="60%" h={16} bg="gray.1" style={{ borderRadius: 4 }} />
<Box w="100%" h={44} bg="gray.1" style={{ borderRadius: 8 }} />
<Box w="100%" h={44} bg="gray.1" style={{ borderRadius: 8 }} />
</>
) : (
children
)}
</Stack> </Stack>
)} )}
</Paper> </Paper>

View File

@ -514,33 +514,6 @@ export const flatToolRegistryMap: ToolRegistry = {
category: "Standard Tools", category: "Standard Tools",
subcategory: "Page Formatting" subcategory: "Page Formatting"
}, },
"split": {
icon: <span className="material-symbols-rounded">content_cut</span>,
name: "home.split.title",
component: null,
view: "format",
description: "home.split.desc",
category: "Standard Tools",
subcategory: "Page Formatting"
},
"split-by-chapters": {
icon: <span className="material-symbols-rounded">collections_bookmark</span>,
name: "home.splitPdfByChapters.title",
component: null,
view: "format",
description: "home.splitPdfByChapters.desc",
category: "Advanced Tools",
subcategory: "Advanced Formatting"
},
"split-by-sections": {
icon: <span className="material-symbols-rounded">grid_on</span>,
name: "home.split-by-sections.title",
component: null,
view: "format",
description: "home.split-by-sections.desc",
category: "Advanced Tools",
subcategory: "Advanced Formatting"
},
"splitPdf": { "splitPdf": {
icon: <span className="material-symbols-rounded">content_cut</span>, icon: <span className="material-symbols-rounded">content_cut</span>,
name: "home.split.title", name: "home.split.title",
@ -594,7 +567,7 @@ function buildStructuredRegistry(): ToolRegistryStructured {
for (const [id, tool] of entries) { for (const [id, tool] of entries) {
const sub = tool.subcategory ?? 'General'; const sub = tool.subcategory ?? 'General';
const cat = tool.category ?? 'OTHER'; const cat = tool.category ?? 'OTHER';
// Quick access: use the existing "Recommended Tools" category // Quick access: use the existing "Recommended Tools" category, this will change in future
if (tool.category === 'Recommended Tools') { if (tool.category === 'Recommended Tools') {
quick.push({ id, ...tool }); quick.push({ id, ...tool });
} }

View File

@ -102,6 +102,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
<ToolStep <ToolStep
title="Settings" title="Settings"
isVisible={hasFiles} isVisible={hasFiles}
enableInitialSkeleton={false}
isCollapsed={settingsCollapsed} isCollapsed={settingsCollapsed}
isCompleted={settingsCollapsed} isCompleted={settingsCollapsed}
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined} onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
@ -129,6 +130,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
<ToolStep <ToolStep
title="Results" title="Results"
isVisible={hasResults} isVisible={hasResults}
enableInitialSkeleton={false}
> >
<Stack gap="sm"> <Stack gap="sm">
{compressOperation.status && ( {compressOperation.status && (

View File

@ -163,6 +163,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
<ToolStep <ToolStep
title={t("convert.results", "Results")} title={t("convert.results", "Results")}
isVisible={hasResults} isVisible={hasResults}
enableInitialSkeleton={false}
data-testid="conversion-results" data-testid="conversion-results"
> >
<Stack gap="sm"> <Stack gap="sm">

View File

@ -96,7 +96,8 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
{/* Settings Step */} {/* Settings Step */}
<ToolStep <ToolStep
title="Settings" title="Settings"
isVisible={hasFiles} isVisible={true}
enableInitialSkeleton={true}
isCollapsed={settingsCollapsed} isCollapsed={settingsCollapsed}
isCompleted={settingsCollapsed} isCompleted={settingsCollapsed}
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined} onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
@ -125,6 +126,7 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
<ToolStep <ToolStep
title="Results" title="Results"
isVisible={hasResults} isVisible={hasResults}
enableInitialSkeleton={false}
> >
<Stack gap="sm"> <Stack gap="sm">
{splitOperation.status && ( {splitOperation.status && (