From 85dedf4b28adb8ac7736109598999472ae852297 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:21:13 +0100 Subject: [PATCH] feat: add mobile slider layout for home page (#4571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix mobile view toggle buttons not working when clicked (use offsetWidth instead of clientWidth) - Add flag to prevent scroll handler interference during programmatic scrolls - Move Files button from header to bottom navigation bar - Add bottom navigation bar with All Tools, Automate, and Files buttons - Add RightRail to mobile workspace view - Auto-switch to Tools view when clicking All Tools or Automate in mobile - Style bottom bar buttons as full-width clickable areas with labels 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Claude --- frontend/src/components/tools/ToolPanel.tsx | 10 +- frontend/src/pages/HomePage.css | 168 ++++++++++++++++ frontend/src/pages/HomePage.tsx | 206 ++++++++++++++++++-- 3 files changed, 364 insertions(+), 20 deletions(-) create mode 100644 frontend/src/pages/HomePage.css diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index 7f19482bd..beefc2c45 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -8,6 +8,7 @@ import { useSidebarContext } from "../../contexts/SidebarContext"; import rainbowStyles from '../../styles/rainbow.module.css'; import { ScrollArea } from '@mantine/core'; import { ToolId } from '../../types/toolId'; +import { useMediaQuery } from '@mantine/hooks'; // No props needed - component uses context @@ -15,6 +16,7 @@ export default function ToolPanel() { const { isRainbowMode } = useRainbowThemeContext(); const { sidebarRefs } = useSidebarContext(); const { toolPanelRef } = sidebarRefs; + const isMobile = useMediaQuery('(max-width: 1024px)'); // Use context-based hooks to eliminate prop drilling @@ -34,17 +36,17 @@ export default function ToolPanel() {
* { + flex: 1 1 auto; + min-height: 0; +} + +.mobile-bottom-bar { + display: flex; + align-items: center; + justify-content: space-around; + padding: 0.5rem; + border-top: 1px solid var(--border-subtle); + background: var(--bg-toolbar); + gap: 0.5rem; + position: relative; + z-index: 10; + touch-action: manipulation; +} + +.mobile-bottom-button { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.25rem; + padding: 0.5rem; + border: none; + background: transparent; + color: var(--text-primary); + cursor: pointer; + border-radius: 0.5rem; + transition: background 0.2s ease; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + user-select: none; + -webkit-user-select: none; + min-height: 44px; +} + +@media (hover: hover) and (pointer: fine) { + .mobile-bottom-button:hover { + background: var(--bg-hover, rgba(0, 0, 0, 0.05)); + } +} + +.mobile-bottom-button:active { + background: var(--bg-active, rgba(0, 0, 0, 0.1)); +} + +.mobile-bottom-button-label { + font-size: 0.75rem; + font-weight: 500; + color: var(--text-muted); +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 26d190dfa..f283a1caa 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,15 +1,24 @@ +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useToolWorkflow } from "../contexts/ToolWorkflowContext"; -import { Group } from "@mantine/core"; +import { Group, useMantineColorScheme } from "@mantine/core"; import { useSidebarContext } from "../contexts/SidebarContext"; import { useDocumentMeta } from "../hooks/useDocumentMeta"; -import { getBaseUrl } from "../constants/app"; +import { BASE_PATH, getBaseUrl } from "../constants/app"; +import { useMediaQuery } from "@mantine/hooks"; +import AppsIcon from '@mui/icons-material/AppsRounded'; import ToolPanel from "../components/tools/ToolPanel"; import Workbench from "../components/layout/Workbench"; import QuickAccessBar from "../components/shared/QuickAccessBar"; import RightRail from "../components/shared/RightRail"; import FileManager from "../components/FileManager"; +import LocalIcon from "../components/shared/LocalIcon"; +import { useFilesModalContext } from "../contexts/FilesModalContext"; + +import "./HomePage.css"; + +type MobileView = "tools" | "workbench"; export default function HomePage() { @@ -20,7 +29,84 @@ export default function HomePage() { const { quickAccessRef } = sidebarRefs; - const { selectedTool, selectedToolKey } = useToolWorkflow(); + const { selectedTool, selectedToolKey, handleToolSelect, handleBackToTools } = useToolWorkflow(); + + const { openFilesModal } = useFilesModalContext(); + const { colorScheme } = useMantineColorScheme(); + const isMobile = useMediaQuery("(max-width: 1024px)"); + const sliderRef = useRef(null); + const [activeMobileView, setActiveMobileView] = useState("tools"); + const isProgrammaticScroll = useRef(false); + + const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo"); + const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${ + colorScheme === "dark" ? "Dark" : "Light" + }.svg`; + const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${ + colorScheme === "dark" ? "White" : "Black" + }Text.svg`; + + const handleSelectMobileView = useCallback((view: MobileView) => { + setActiveMobileView(view); + }, []); + + useEffect(() => { + if (isMobile) { + const container = sliderRef.current; + if (container) { + isProgrammaticScroll.current = true; + const offset = activeMobileView === "tools" ? 0 : container.offsetWidth; + container.scrollTo({ left: offset, behavior: "smooth" }); + + // Re-enable scroll listener after animation completes + setTimeout(() => { + isProgrammaticScroll.current = false; + }, 500); + } + return; + } + + setActiveMobileView("tools"); + const container = sliderRef.current; + if (container) { + container.scrollTo({ left: 0, behavior: "auto" }); + } + }, [activeMobileView, isMobile]); + + useEffect(() => { + if (!isMobile) return; + + const container = sliderRef.current; + if (!container) return; + + let animationFrame = 0; + + const handleScroll = () => { + if (isProgrammaticScroll.current) { + return; + } + + if (animationFrame) { + cancelAnimationFrame(animationFrame); + } + + animationFrame = window.requestAnimationFrame(() => { + const { scrollLeft, offsetWidth } = container; + const threshold = offsetWidth / 2; + const nextView: MobileView = scrollLeft >= threshold ? "workbench" : "tools"; + setActiveMobileView((current) => (current === nextView ? current : nextView)); + }); + }; + + container.addEventListener("scroll", handleScroll, { passive: true }); + + return () => { + container.removeEventListener("scroll", handleScroll); + if (animationFrame) { + cancelAnimationFrame(animationFrame); + } + }; + }, [isMobile]); const baseUrl = getBaseUrl(); @@ -38,19 +124,107 @@ export default function HomePage() { return (
- - - - - - - + {isMobile ? ( +
+
+
+
+ + {brandAltText} +
+
+
+ + +
+ + {t('home.mobile.swipeHint', 'Swipe left or right to switch views')} + +
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+ + + +
+ +
+ ) : ( + + + + + + + + )}
); }