Skip to content

Commit c672147

Browse files
committed
refactor(HomePage, SkeletonLoader, ToolPanelSkeleton): improve loading states and adjust layout for better responsiveness
1 parent fc97d0c commit c672147

7 files changed

Lines changed: 99 additions & 22 deletions

File tree

frontend/src/core/components/layout/Workbench.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Suspense, lazy, useCallback } from "react";
2-
import { Box, Loader, Center } from "@mantine/core";
2+
import { Box } from "@mantine/core";
33
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
44
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
55
import { useFileHandler } from "@app/hooks/useFileHandler";
@@ -20,6 +20,8 @@ import LandingPage from "@app/components/shared/LandingPage";
2020
import Footer from "@app/components/shared/Footer";
2121
import DismissAllErrorsButton from "@app/components/shared/DismissAllErrorsButton";
2222

23+
import SkeletonLoader from "@app/components/shared/SkeletonLoader";
24+
2325
// Workbench panels are loaded on demand. Viewer pulls in pdfjs-dist and the
2426
// full @embedpdf plugin set; FileEditor/PageEditor are only needed once a file
2527
// is open. Lazy-loading keeps all of that out of the initial bundle.
@@ -244,9 +246,19 @@ export default function Workbench() {
244246
>
245247
<Suspense
246248
fallback={
247-
<Center style={{ height: "100%" }}>
248-
<Loader />
249-
</Center>
249+
<Box p="md" h="100%">
250+
<SkeletonLoader
251+
type={
252+
currentView === "viewer"
253+
? "viewer"
254+
: currentView === "fileEditor"
255+
? "fileGrid"
256+
: currentView === "pageEditor"
257+
? "pageGrid"
258+
: "block"
259+
}
260+
/>
261+
</Box>
250262
}
251263
>
252264
{renderMainContent()}

frontend/src/core/components/shared/SkeletonLoader.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22
import { Box, Group, Stack } from "@mantine/core";
33

44
interface SkeletonLoaderProps {
5-
type: "pageGrid" | "fileGrid" | "controls" | "viewer" | "block";
5+
type: "pageGrid" | "fileGrid" | "controls" | "viewer" | "block" | "toolList";
66
count?: number;
77
animated?: boolean;
88
width?: number | string;
@@ -35,6 +35,36 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
3535
/>
3636
);
3737

38+
const renderToolListSkeleton = () => (
39+
<Stack gap="xs" p="sm" h="100%" w="100%">
40+
{/* Search bar placeholder */}
41+
<Box
42+
h={40}
43+
bg="gray.1"
44+
style={{ borderRadius: 8, ...animationStyle }}
45+
mb="xs"
46+
/>
47+
48+
{/* List items */}
49+
{Array.from({ length: count }).map((_, i) => (
50+
<Group key={i} wrap="nowrap" gap="sm" py={4}>
51+
<Box
52+
w={32}
53+
h={32}
54+
bg="gray.1"
55+
style={{ borderRadius: 6, ...animationStyle }}
56+
/>
57+
<Box
58+
flex={1}
59+
h={20}
60+
bg="gray.1"
61+
style={{ borderRadius: 4, ...animationStyle }}
62+
/>
63+
</Group>
64+
))}
65+
</Stack>
66+
);
67+
3868
const renderPageGridSkeleton = () => (
3969
<div
4070
style={{
@@ -150,6 +180,8 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
150180
switch (type) {
151181
case "block":
152182
return renderBlock();
183+
case "toolList":
184+
return renderToolListSkeleton();
153185
case "pageGrid":
154186
return renderPageGridSkeleton();
155187
case "fileGrid":

frontend/src/core/components/tools/SearchResults.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const SearchResults: React.FC<SearchResultsProps> = ({
7878
</Box>
7979
))}
8080
{/* Global spacer to allow scrolling past last row in search mode */}
81-
<div aria-hidden style={{ height: 200 }} />
81+
<div aria-hidden style={{ height: 64 }} />
8282
</Stack>
8383
);
8484
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import SkeletonLoader from "@app/components/shared/SkeletonLoader";
2+
import { useIsMobile } from "@app/hooks/useIsMobile";
3+
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
4+
5+
export default function ToolPanelSkeleton() {
6+
const isMobile = useIsMobile();
7+
const { isPanelVisible } = useToolWorkflow();
8+
9+
const width = isMobile ? "100%" : isPanelVisible ? "18.5rem" : "0";
10+
11+
return (
12+
<div
13+
className="tool-panel-skeleton"
14+
style={{
15+
width,
16+
height: isMobile ? "100%" : "100vh",
17+
backgroundColor: "var(--bg-toolbar)",
18+
borderRight: isMobile ? "none" : "1px solid var(--border-subtle)",
19+
overflow: "hidden",
20+
transition: "width 0.3s ease",
21+
flexShrink: 0,
22+
}}
23+
>
24+
<SkeletonLoader type="toolList" count={12} />
25+
</div>
26+
);
27+
}

frontend/src/core/components/tools/ToolPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ const ToolPicker = ({
183183
{!quickSection && !allSection && <NoToolsFound />}
184184

185185
{/* bottom spacer to allow scrolling past the last row */}
186-
<div aria-hidden style={{ height: 200 }} />
186+
<div aria-hidden style={{ height: 64 }} />
187187
</>
188188
)}
189189
</Box>

frontend/src/core/pages/HomePage.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@
3333
height: 1.5rem;
3434
width: auto;
3535
flex-shrink: 0;
36+
aspect-ratio: 1 / 1;
3637
}
3738

3839
.mobile-brand-text {
3940
height: 1.5rem;
4041
width: auto;
4142
max-width: 100%;
43+
aspect-ratio: 4 / 1; /* Wordmark usually has around 4:1 ratio */
4244
}
4345

4446
.mobile-toggle-buttons {
@@ -54,14 +56,18 @@
5456
.mobile-toggle-button {
5557
border: none;
5658
border-radius: 9999px;
57-
padding: 0.4rem 0.9rem;
58-
font-size: 0.8125rem;
59+
padding: 0.6rem 1.25rem;
60+
font-size: 0.875rem;
5961
font-weight: 600;
6062
color: var(--text-muted);
6163
background: transparent;
6264
transition:
6365
background 0.2s ease,
6466
color 0.2s ease;
67+
min-height: 44px;
68+
display: flex;
69+
align-items: center;
70+
justify-content: center;
6571
}
6672

6773
.mobile-toggle-button:focus-visible {

frontend/src/core/pages/HomePage.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,14 @@ import {
2424
import { useViewer } from "@app/contexts/ViewerContext";
2525
import AppsIcon from "@mui/icons-material/AppsRounded";
2626

27-
import ToolPanel from "@app/components/tools/ToolPanel";
28-
import Workbench from "@app/components/layout/Workbench";
2927
import QuickAccessBar from "@app/components/shared/QuickAccessBar";
3028
import RightRail from "@app/components/shared/RightRail";
31-
import FileManager from "@app/components/FileManager";
3229
import LocalIcon from "@app/components/shared/LocalIcon";
3330
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
34-
import AppConfigModal from "@app/components/shared/AppConfigModalLazy";
3531
import { getStartupNavigationAction } from "@app/utils/homePageNavigation";
3632
import { HomePageExtensions } from "@app/components/home/HomePageExtensions";
37-
import LocalIcon from "@app/components/shared/LocalIcon";
38-
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
39-
40-
import QuickAccessBar from "@app/components/shared/QuickAccessBar";
41-
import RightRail from "@app/components/shared/RightRail";
4233
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
34+
import ToolPanelSkeleton from "@app/components/tools/ToolPanelSkeleton";
4335

4436
import "@app/pages/HomePage.css";
4537

@@ -150,8 +142,14 @@ export default function HomePage() {
150142
if (isMobile) {
151143
const container = sliderRef.current;
152144
if (container) {
153-
isProgrammaticScroll.current = true;
154145
const offset = activeMobileView === "tools" ? 0 : container.offsetWidth;
146+
147+
// Skip if already at the target position to avoid fighting with user swipes
148+
if (Math.abs(container.scrollLeft - offset) < 10) {
149+
return;
150+
}
151+
152+
isProgrammaticScroll.current = true;
155153
container.scrollTo({ left: offset, behavior: "smooth" });
156154

157155
// Re-enable scroll listener after animation completes
@@ -165,7 +163,9 @@ export default function HomePage() {
165163
setActiveMobileView("tools");
166164
const container = sliderRef.current;
167165
if (container) {
168-
container.scrollTo({ left: 0, behavior: "auto" });
166+
if (container.scrollLeft !== 0) {
167+
container.scrollTo({ left: 0, behavior: "auto" });
168+
}
169169
}
170170
}, [activeMobileView, isMobile]);
171171

@@ -311,7 +311,7 @@ export default function HomePage() {
311311
aria-label={t("home.mobile.toolsSlide", "Tool selection panel")}
312312
>
313313
<div className="mobile-slide-content">
314-
<Suspense fallback={<LoadingFallback />}>
314+
<Suspense fallback={<ToolPanelSkeleton />}>
315315
<ToolPanel />
316316
</Suspense>
317317
</div>
@@ -406,7 +406,7 @@ export default function HomePage() {
406406
<Group align="flex-start" gap={0} h="100%" className="flex-nowrap flex">
407407
<QuickAccessBar ref={quickAccessRef} />
408408
{!hideToolPanel && (
409-
<Suspense fallback={<LoadingFallback />}>
409+
<Suspense fallback={<ToolPanelSkeleton />}>
410410
<ToolPanel />
411411
</Suspense>
412412
)}

0 commit comments

Comments
 (0)