Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/src/core/components/FileManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
import { loadScript } from "@app/utils/scriptLoader";
import { useAllFiles } from "@app/contexts/FileContext";

import { ToolRegistryEntry } from "@app/data/toolsTaxonomy";

interface FileManagerProps {
selectedTool?: Tool | null;
selectedTool?: Tool | ToolRegistryEntry | null;
}

const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
Expand Down
20 changes: 16 additions & 4 deletions frontend/src/core/components/layout/Workbench.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Suspense, lazy, useCallback } from "react";
import { Box, Loader, Center } from "@mantine/core";
import { Box } from "@mantine/core";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useFileHandler } from "@app/hooks/useFileHandler";
Expand All @@ -20,6 +20,8 @@ import LandingPage from "@app/components/shared/LandingPage";
import Footer from "@app/components/shared/Footer";
import DismissAllErrorsButton from "@app/components/shared/DismissAllErrorsButton";

import SkeletonLoader from "@app/components/shared/SkeletonLoader";

// Workbench panels are loaded on demand. Viewer pulls in pdfjs-dist and the
// full @embedpdf plugin set; FileEditor/PageEditor are only needed once a file
// is open. Lazy-loading keeps all of that out of the initial bundle.
Expand Down Expand Up @@ -244,9 +246,19 @@ export default function Workbench() {
>
<Suspense
fallback={
<Center style={{ height: "100%" }}>
<Loader />
</Center>
<Box p="md" h="100%">
<SkeletonLoader
type={
currentView === "viewer"
? "viewer"
: currentView === "fileEditor"
? "fileGrid"
: currentView === "pageEditor"
? "pageGrid"
: "block"
}
/>
</Box>
}
>
{renderMainContent()}
Expand Down
14 changes: 10 additions & 4 deletions frontend/src/core/components/shared/LoadingFallback.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
/**
* Loading fallback component for i18next suspense
* Loading fallback component for i18next suspense and lazy components.
* Uses 100% height to fill its parent container without causing layout shifts
* by forcing a 100vh height in contained areas.
*/
export function LoadingFallback() {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100vh",
height: "100%",
width: "100%",
minHeight: "200px",
fontSize: "18px",
color: "#666",
color: "var(--text-muted, #666)",
background: "transparent",
}}
>
Loading...
<div className="animate-pulse">Loading...</div>
</div>
);
}
34 changes: 33 additions & 1 deletion frontend/src/core/components/shared/SkeletonLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import { Box, Group, Stack } from "@mantine/core";

interface SkeletonLoaderProps {
type: "pageGrid" | "fileGrid" | "controls" | "viewer" | "block";
type: "pageGrid" | "fileGrid" | "controls" | "viewer" | "block" | "toolList";
count?: number;
animated?: boolean;
width?: number | string;
Expand Down Expand Up @@ -35,6 +35,36 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
/>
);

const renderToolListSkeleton = () => (
<Stack gap="xs" p="sm" h="100%" w="100%">
{/* Search bar placeholder */}
<Box
h={40}
bg="gray.1"
style={{ borderRadius: 8, ...animationStyle }}
mb="xs"
/>

{/* List items */}
{Array.from({ length: count }).map((_, i) => (
<Group key={i} wrap="nowrap" gap="sm" py={4}>
<Box
w={32}
h={32}
bg="gray.1"
style={{ borderRadius: 6, ...animationStyle }}
/>
<Box
flex={1}
h={20}
bg="gray.1"
style={{ borderRadius: 4, ...animationStyle }}
/>
</Group>
))}
</Stack>
);

const renderPageGridSkeleton = () => (
<div
style={{
Expand Down Expand Up @@ -150,6 +180,8 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
switch (type) {
case "block":
return renderBlock();
case "toolList":
return renderToolListSkeleton();
case "pageGrid":
return renderPageGridSkeleton();
case "fileGrid":
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/core/components/tools/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const SearchResults: React.FC<SearchResultsProps> = ({
</Box>
))}
{/* Global spacer to allow scrolling past last row in search mode */}
<div aria-hidden style={{ height: 200 }} />
<div aria-hidden style={{ height: 64 }} />
</Stack>
);
};
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/core/components/tools/ToolPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ import { useRightRail } from "@app/contexts/RightRailContext";
import { Tooltip } from "@app/components/shared/Tooltip";
import "@app/components/tools/ToolPanel.css";

// No props needed - component uses context
interface ToolPanelProps {
isMobile?: boolean;
}

export default function ToolPanel() {
export default function ToolPanel({ isMobile: isMobileProp }: ToolPanelProps) {
const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext();
const { sidebarRefs } = useSidebarContext();
const { toolPanelRef, quickAccessRef, rightRailRef } = sidebarRefs;
const isMobile = useIsMobile();
const isMobileHook = useIsMobile();
const isMobile = isMobileProp ?? isMobileHook;

const {
leftPanelView,
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/core/components/tools/ToolPanelSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import SkeletonLoader from "@app/components/shared/SkeletonLoader";
import { useIsMobile } from "@app/hooks/useIsMobile";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";

interface ToolPanelSkeletonProps {
isMobile?: boolean;
}

export default function ToolPanelSkeleton({
isMobile: isMobileProp,
}: ToolPanelSkeletonProps) {
const isMobileHook = useIsMobile();
const isMobile = isMobileProp ?? isMobileHook;
const { isPanelVisible } = useToolWorkflow();

const width = isMobile ? "100%" : isPanelVisible ? "18.5rem" : "0";

return (
<div
className="tool-panel-skeleton"
style={{
width,
height: isMobile ? "100%" : "100vh",
backgroundColor: "var(--bg-toolbar)",
borderRight: isMobile ? "none" : "1px solid var(--border-subtle)",
overflow: "hidden",
flexShrink: 0,
}}
>
<SkeletonLoader type="toolList" count={12} />
</div>
);
}
2 changes: 1 addition & 1 deletion frontend/src/core/components/tools/ToolPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const ToolPicker = ({
{!quickSection && !allSection && <NoToolsFound />}

{/* bottom spacer to allow scrolling past the last row */}
<div aria-hidden style={{ height: 200 }} />
<div aria-hidden style={{ height: 64 }} />
</>
)}
</Box>
Expand Down
Loading
Loading