Ver Fonte

* Add comprehensive mobile support with responsive navigation
- Add hamburger drawer navigation for mobile (< 768px)
- Create useIsMobile hook for responsive breakpoint detection
- Create useLongPress hook for touch gesture context menus
- Update Button component with 44px min-height for WCAG touch compliance
- Update Toggle component with larger mobile size (44x28px)
- Add responsive padding (p-4 md:p-8) to all pages
- Update ArchivesPage with mobile menu button and horizontal scroll filters
- Update SettingsPage with stacked column layout on mobile
- Increase drag handle sizes for touch-friendly reordering
- Add CSS utilities for touch-manipulation and safe-area insets

maziggy há 5 meses atrás
pai
commit
42cdfdbd74

+ 3 - 3
frontend/src/components/Button.tsx

@@ -26,9 +26,9 @@ export function Button({
   };
 
   const sizes = {
-    sm: 'px-3 py-1.5 text-sm gap-1.5',
-    md: 'px-4 py-2 text-sm gap-2',
-    lg: 'px-6 py-3 text-base gap-2',
+    sm: 'px-3 py-1.5 text-sm gap-1.5 min-h-[44px] md:min-h-0',
+    md: 'px-4 py-2 text-sm gap-2 min-h-[44px] md:min-h-0',
+    lg: 'px-6 py-3 text-base gap-2 min-h-[48px] md:min-h-0',
   };
 
   return (

+ 1 - 1
frontend/src/components/Dashboard.tsx

@@ -93,7 +93,7 @@ function SortableWidget({
             className="cursor-grab active:cursor-grabbing p-1 hover:bg-bambu-dark-tertiary rounded transition-colors"
             title="Drag to reorder"
           >
-            <GripVertical className="w-4 h-4 text-bambu-gray" />
+            <GripVertical className="w-6 h-6 md:w-4 md:h-4 text-bambu-gray" />
           </button>
           <h3 className="text-sm font-medium text-white">{title}</h3>
         </div>

+ 1 - 1
frontend/src/components/ExternalLinksSettings.tsx

@@ -117,7 +117,7 @@ export function ExternalLinksSettings() {
                       draggedId === link.id ? 'opacity-50' : ''
                     }`}
                   >
-                    <GripVertical className="w-4 h-4 text-bambu-gray cursor-grab flex-shrink-0" />
+                    <GripVertical className="w-6 h-6 md:w-4 md:h-4 text-bambu-gray cursor-grab flex-shrink-0" />
                     <div className="p-2 rounded-lg bg-bambu-dark-tertiary text-bambu-gray">
                       <Icon className="w-4 h-4" />
                     </div>

+ 71 - 27
frontend/src/components/Layout.tsx

@@ -1,12 +1,13 @@
 import { useState, useEffect, useCallback, useRef } from 'react';
 import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
-import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, X, type LucideIcon } from 'lucide-react';
+import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, X, Menu, type LucideIcon } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTheme } from '../contexts/ThemeContext';
 import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
 import { useQuery } from '@tanstack/react-query';
 import { api } from '../api/client';
 import { getIconByName } from './IconPicker';
+import { useIsMobile } from '../hooks/useIsMobile';
 
 interface NavItem {
   id: string;
@@ -63,10 +64,12 @@ export function Layout() {
   const location = useLocation();
   const { theme, toggleTheme } = useTheme();
   const { t } = useTranslation();
+  const isMobile = useIsMobile();
   const [sidebarExpanded, setSidebarExpanded] = useState(() => {
     const stored = localStorage.getItem('sidebarExpanded');
     return stored !== 'false';
   });
+  const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
   const [showShortcuts, setShowShortcuts] = useState(false);
   const [sidebarOrder, setSidebarOrder] = useState<string[]>(getSidebarOrder);
   const [draggedId, setDraggedId] = useState<string | null>(null);
@@ -219,6 +222,13 @@ export function Layout() {
     localStorage.setItem('sidebarExpanded', String(sidebarExpanded));
   }, [sidebarExpanded]);
 
+  // Close mobile drawer on navigation
+  useEffect(() => {
+    if (isMobile) {
+      setMobileDrawerOpen(false);
+    }
+  }, [location.pathname, isMobile]);
+
   // Global keyboard shortcuts for navigation
   const handleKeyDown = useCallback((e: KeyboardEvent) => {
     const target = e.target as HTMLElement;
@@ -259,16 +269,46 @@ export function Layout() {
 
   return (
     <div className="flex min-h-screen">
-      {/* Sidebar */}
+      {/* Mobile Header */}
+      {isMobile && (
+        <header className="fixed top-0 left-0 right-0 z-40 h-14 bg-bambu-dark-secondary border-b border-bambu-dark-tertiary flex items-center px-4">
+          <button
+            onClick={() => setMobileDrawerOpen(true)}
+            className="p-2 -ml-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors"
+            aria-label="Open menu"
+          >
+            <Menu className="w-6 h-6 text-white" />
+          </button>
+          <img
+            src={theme === 'dark' ? '/img/bambuddy_logo_dark.png' : '/img/bambuddy_logo_light.png'}
+            alt="Bambuddy"
+            className="h-8 ml-3"
+          />
+        </header>
+      )}
+
+      {/* Mobile Drawer Backdrop */}
+      {isMobile && mobileDrawerOpen && (
+        <div
+          className="fixed inset-0 bg-black/60 z-40 transition-opacity"
+          onClick={() => setMobileDrawerOpen(false)}
+        />
+      )}
+
+      {/* Sidebar / Mobile Drawer */}
       <aside
-        className={`${sidebarExpanded ? 'w-64' : 'w-16'} bg-bambu-dark-secondary border-r border-bambu-dark-tertiary flex flex-col fixed inset-y-0 left-0 z-30 transition-all duration-300`}
+        className={`bg-bambu-dark-secondary border-r border-bambu-dark-tertiary flex flex-col transition-all duration-300 ${
+          isMobile
+            ? `fixed inset-y-0 left-0 z-50 w-72 transform ${mobileDrawerOpen ? 'translate-x-0' : '-translate-x-full'}`
+            : `fixed inset-y-0 left-0 z-30 ${sidebarExpanded ? 'w-64' : 'w-16'}`
+        }`}
       >
         {/* Logo */}
-        <div className={`border-b border-bambu-dark-tertiary flex items-center justify-center ${sidebarExpanded ? 'p-4' : 'p-2'}`}>
+        <div className={`border-b border-bambu-dark-tertiary flex items-center justify-center ${isMobile || sidebarExpanded ? 'p-4' : 'p-2'}`}>
           <img
             src={theme === 'dark' ? '/img/bambuddy_logo_dark.png' : '/img/bambuddy_logo_light.png'}
             alt="Bambuddy"
-            className={sidebarExpanded ? 'h-16 w-auto' : 'h-8 w-8 object-cover object-left'}
+            className={isMobile || sidebarExpanded ? 'h-16 w-auto' : 'h-8 w-8 object-cover object-left'}
           />
         </div>
 
@@ -304,15 +344,15 @@ export function Layout() {
                     <NavLink
                       to={`/external/${link.id}`}
                       className={({ isActive }) =>
-                        `flex items-center ${sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group ${
+                        `flex items-center ${isMobile || sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group ${
                           isActive
                             ? 'bg-bambu-green text-white'
                             : 'text-bambu-gray-light hover:bg-bambu-dark-tertiary hover:text-white'
                         }`
                       }
-                      title={!sidebarExpanded ? link.name : undefined}
+                      title={!isMobile && !sidebarExpanded ? link.name : undefined}
                     >
-                      {sidebarExpanded && (
+                      {sidebarExpanded && !isMobile && (
                         <GripVertical className="w-4 h-4 flex-shrink-0 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing -ml-1" />
                       )}
                       {link.custom_icon ? (
@@ -324,7 +364,7 @@ export function Layout() {
                       ) : (
                         LinkIcon && <LinkIcon className="w-5 h-5 flex-shrink-0" />
                       )}
-                      {sidebarExpanded && <span>{link.name}</span>}
+                      {(isMobile || sidebarExpanded) && <span>{link.name}</span>}
                     </NavLink>
                   </li>
                 );
@@ -354,19 +394,19 @@ export function Layout() {
                     <NavLink
                       to={to}
                       className={({ isActive }) =>
-                        `flex items-center ${sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group ${
+                        `flex items-center ${isMobile || sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group ${
                           isActive
                             ? 'bg-bambu-green text-white'
                             : 'text-bambu-gray-light hover:bg-bambu-dark-tertiary hover:text-white'
                         }`
                       }
-                      title={!sidebarExpanded ? t(labelKey) : undefined}
+                      title={!isMobile && !sidebarExpanded ? t(labelKey) : undefined}
                     >
-                      {sidebarExpanded && (
+                      {sidebarExpanded && !isMobile && (
                         <GripVertical className="w-4 h-4 flex-shrink-0 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing -ml-1" />
                       )}
                       <Icon className="w-5 h-5 flex-shrink-0" />
-                      {sidebarExpanded && <span>{t(labelKey)}</span>}
+                      {(isMobile || sidebarExpanded) && <span>{t(labelKey)}</span>}
                     </NavLink>
                   </li>
                 );
@@ -375,22 +415,24 @@ export function Layout() {
           </ul>
         </nav>
 
-        {/* Collapse toggle */}
-        <button
-          onClick={() => setSidebarExpanded(!sidebarExpanded)}
-          className="p-2 mx-2 mb-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors text-bambu-gray-light hover:text-white flex items-center justify-center"
-          title={sidebarExpanded ? t('nav.collapseSidebar') : t('nav.expandSidebar')}
-        >
-          {sidebarExpanded ? (
-            <ChevronLeft className="w-5 h-5" />
-          ) : (
-            <ChevronRight className="w-5 h-5" />
-          )}
-        </button>
+        {/* Collapse toggle - hide on mobile */}
+        {!isMobile && (
+          <button
+            onClick={() => setSidebarExpanded(!sidebarExpanded)}
+            className="p-2 mx-2 mb-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors text-bambu-gray-light hover:text-white flex items-center justify-center"
+            title={sidebarExpanded ? t('nav.collapseSidebar') : t('nav.expandSidebar')}
+          >
+            {sidebarExpanded ? (
+              <ChevronLeft className="w-5 h-5" />
+            ) : (
+              <ChevronRight className="w-5 h-5" />
+            )}
+          </button>
+        )}
 
         {/* Footer */}
         <div className="p-2 border-t border-bambu-dark-tertiary">
-          {sidebarExpanded ? (
+          {isMobile || sidebarExpanded ? (
             <div className="flex items-center justify-between px-2">
               <div className="flex items-center gap-2">
                 <span className="text-sm text-bambu-gray">v{versionInfo?.version || '...'}</span>
@@ -471,7 +513,9 @@ export function Layout() {
       </aside>
 
       {/* Main content */}
-      <main className={`flex-1 bg-bambu-dark overflow-auto ${sidebarExpanded ? 'ml-64' : 'ml-16'} transition-all duration-300`}>
+      <main className={`flex-1 bg-bambu-dark overflow-auto transition-all duration-300 ${
+        isMobile ? 'mt-14' : sidebarExpanded ? 'ml-64' : 'ml-16'
+      }`}>
         {/* Persistent update banner */}
         {showUpdateBanner && (
           <div className="bg-bambu-green/20 border-b border-bambu-green/30 px-4 py-2 flex items-center justify-between">

+ 2 - 2
frontend/src/components/Toggle.tsx

@@ -20,7 +20,7 @@ export function Toggle({ checked, onChange, disabled }: ToggleProps) {
       aria-checked={checked}
       disabled={disabled}
       onClick={handleClick}
-      className={`relative inline-flex w-9 h-5 rounded-full transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${
+      className={`relative inline-flex w-11 h-7 md:w-9 md:h-5 rounded-full transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${
         disabled
           ? 'bg-bambu-dark-tertiary/50 cursor-not-allowed opacity-50'
           : checked
@@ -29,7 +29,7 @@ export function Toggle({ checked, onChange, disabled }: ToggleProps) {
       }`}
     >
       <span
-        className={`pointer-events-none absolute top-[2px] left-[2px] w-4 h-4 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${
+        className={`pointer-events-none absolute top-[3px] md:top-[2px] left-[3px] md:left-[2px] w-5 h-5 md:w-4 md:h-4 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${
           checked ? 'translate-x-4' : 'translate-x-0'
         }`}
       />

+ 26 - 0
frontend/src/hooks/useIsMobile.ts

@@ -0,0 +1,26 @@
+import { useState, useEffect } from 'react';
+
+const MOBILE_BREAKPOINT = 768; // md breakpoint
+
+export function useIsMobile(): boolean {
+  const [isMobile, setIsMobile] = useState(() =>
+    typeof window !== 'undefined' ? window.innerWidth < MOBILE_BREAKPOINT : false
+  );
+
+  useEffect(() => {
+    const mediaQuery = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+
+    const handleChange = (e: MediaQueryListEvent) => {
+      setIsMobile(e.matches);
+    };
+
+    // Set initial value
+    setIsMobile(mediaQuery.matches);
+
+    // Modern browsers support addEventListener
+    mediaQuery.addEventListener('change', handleChange);
+    return () => mediaQuery.removeEventListener('change', handleChange);
+  }, []);
+
+  return isMobile;
+}

+ 46 - 0
frontend/src/hooks/useLongPress.ts

@@ -0,0 +1,46 @@
+import { useCallback, useRef } from 'react';
+
+interface LongPressOptions {
+  onLongPress: (e: React.TouchEvent | React.MouseEvent) => void;
+  onClick?: () => void;
+  delay?: number;
+}
+
+export function useLongPress({ onLongPress, onClick, delay = 500 }: LongPressOptions) {
+  const timeoutRef = useRef<number | null>(null);
+  const targetRef = useRef<EventTarget | null>(null);
+  const longPressTriggered = useRef(false);
+
+  const start = useCallback(
+    (e: React.TouchEvent | React.MouseEvent) => {
+      longPressTriggered.current = false;
+      targetRef.current = e.target;
+      timeoutRef.current = window.setTimeout(() => {
+        longPressTriggered.current = true;
+        onLongPress(e);
+      }, delay);
+    },
+    [onLongPress, delay]
+  );
+
+  const clear = useCallback(
+    (e: React.TouchEvent | React.MouseEvent, shouldTriggerClick = true) => {
+      if (timeoutRef.current) {
+        clearTimeout(timeoutRef.current);
+        timeoutRef.current = null;
+      }
+      if (shouldTriggerClick && !longPressTriggered.current && onClick && targetRef.current === e.target) {
+        onClick();
+      }
+    },
+    [onClick]
+  );
+
+  return {
+    onMouseDown: start,
+    onMouseUp: (e: React.MouseEvent) => clear(e, true),
+    onMouseLeave: (e: React.MouseEvent) => clear(e, false),
+    onTouchStart: start,
+    onTouchEnd: (e: React.TouchEvent) => clear(e, true),
+  };
+}

+ 38 - 0
frontend/src/index.css

@@ -143,3 +143,41 @@ body {
     #333 4px
   );
 }
+
+/* Touch manipulation to prevent zoom on double-tap */
+.touch-manipulation {
+  touch-action: manipulation;
+}
+
+/* Safe area insets for notched devices */
+.safe-area-bottom {
+  padding-bottom: env(safe-area-inset-bottom, 0);
+}
+
+.safe-area-top {
+  padding-top: env(safe-area-inset-top, 0);
+}
+
+/* Hide scrollbar but keep functionality */
+.scrollbar-hide {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+.scrollbar-hide::-webkit-scrollbar {
+  display: none;
+}
+
+/* Mobile drawer animation */
+@keyframes slide-in-left {
+  from {
+    transform: translateX(-100%);
+  }
+  to {
+    transform: translateX(0);
+  }
+}
+
+.animate-slide-in-left {
+  animation: slide-in-left 0.3s ease-out;
+}

+ 37 - 16
frontend/src/pages/ArchivesPage.tsx

@@ -35,8 +35,10 @@ import {
   Camera,
   FileText,
   FileCode,
+  MoreVertical,
 } from 'lucide-react';
 import { api } from '../api/client';
+import { useIsMobile } from '../hooks/useIsMobile';
 import type { Archive } from '../api/client';
 import { Card, CardContent } from '../components/Card';
 import { Button } from '../components/Button';
@@ -93,6 +95,7 @@ function ArchiveCard({
 }) {
   const queryClient = useQueryClient();
   const { showToast } = useToast();
+  const isMobile = useIsMobile();
   const [showViewer, setShowViewer] = useState(false);
   const [showReprint, setShowReprint] = useState(false);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -355,6 +358,20 @@ function ArchiveCard({
             <Image className="w-12 h-12 text-bambu-dark-tertiary" />
           </div>
         )}
+        {/* Mobile menu button */}
+        {isMobile && (
+          <button
+            className="absolute top-2 right-10 p-1.5 rounded bg-black/50 hover:bg-black/70 transition-colors"
+            onClick={(e) => {
+              e.stopPropagation();
+              const rect = e.currentTarget.getBoundingClientRect();
+              setContextMenu({ x: rect.left, y: rect.bottom + 4 });
+            }}
+            title="Menu"
+          >
+            <MoreVertical className="w-5 h-5 text-white" />
+          </button>
+        )}
         {/* Favorite star */}
         <button
           className="absolute top-2 right-2 p-1 rounded bg-black/50 hover:bg-black/70 transition-colors"
@@ -1069,7 +1086,7 @@ export function ArchivesPage() {
 
   return (
     <div
-      className="p-8 relative min-h-full"
+      className="p-4 md:p-8 relative min-h-full"
       onDragOver={handleDragOver}
       onDragLeave={handleDragLeave}
       onDrop={handleDrop}
@@ -1175,20 +1192,23 @@ export function ArchivesPage() {
       {/* Filters */}
       <Card className="mb-6">
         <CardContent className="py-4">
-          <div className="flex gap-4 items-center flex-wrap">
-            <div className="flex-1 relative min-w-[200px]">
+          <div className="flex flex-col md:flex-row gap-3 md:gap-4 md:items-center md:flex-wrap">
+            {/* Search - full width on mobile */}
+            <div className="w-full md:flex-1 relative md:min-w-[200px]">
               <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray" />
               <input
                 ref={searchInputRef}
                 type="text"
-                placeholder="Search archives... (press /)"
-                className="w-full pl-10 pr-4 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
+                placeholder="Search archives..."
+                className="w-full pl-10 pr-4 py-3 md:py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 value={search}
                 onChange={(e) => setSearch(e.target.value)}
               />
             </div>
-            <div className="flex items-center gap-2">
-              <Filter className="w-4 h-4 text-bambu-gray" />
+            {/* Filters - horizontal scroll on mobile */}
+            <div className="flex gap-2 md:gap-4 overflow-x-auto pb-1 md:pb-0 -mx-4 px-4 md:mx-0 md:px-0 md:flex-wrap scrollbar-hide">
+            <div className="flex items-center gap-2 flex-shrink-0">
+              <Filter className="w-4 h-4 text-bambu-gray hidden md:block" />
               <select
                 className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 value={filterPrinter || ''}
@@ -1204,8 +1224,8 @@ export function ArchivesPage() {
                 ))}
               </select>
             </div>
-            <div className="flex items-center gap-2">
-              <Package className="w-4 h-4 text-bambu-gray" />
+            <div className="flex items-center gap-2 flex-shrink-0">
+              <Package className="w-4 h-4 text-bambu-gray hidden md:block" />
               <select
                 className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 value={filterMaterial || ''}
@@ -1223,7 +1243,7 @@ export function ArchivesPage() {
             </div>
             <button
               onClick={() => setFilterFavorites(!filterFavorites)}
-              className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors ${
+              className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors flex-shrink-0 ${
                 filterFavorites
                   ? 'bg-yellow-500/20 border-yellow-500 text-yellow-400'
                   : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
@@ -1231,11 +1251,11 @@ export function ArchivesPage() {
               title={filterFavorites ? 'Show all' : 'Show favorites only'}
             >
               <Star className={`w-4 h-4 ${filterFavorites ? 'fill-yellow-400' : ''}`} />
-              <span className="text-sm">Favorites</span>
+              <span className="text-sm hidden md:inline">Favorites</span>
             </button>
             {uniqueTags.length > 0 && (
-              <div className="flex items-center gap-2">
-                <Tag className="w-4 h-4 text-bambu-gray" />
+              <div className="flex items-center gap-2 flex-shrink-0">
+                <Tag className="w-4 h-4 text-bambu-gray hidden md:block" />
                 <select
                   className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                   value={filterTag || ''}
@@ -1250,8 +1270,8 @@ export function ArchivesPage() {
                 </select>
               </div>
             )}
-            <div className="flex items-center gap-2">
-              <ArrowUpDown className="w-4 h-4 text-bambu-gray" />
+            <div className="flex items-center gap-2 flex-shrink-0">
+              <ArrowUpDown className="w-4 h-4 text-bambu-gray hidden md:block" />
               <select
                 className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 value={sortBy}
@@ -1265,7 +1285,7 @@ export function ArchivesPage() {
                 <option value="size-asc">Smallest first</option>
               </select>
             </div>
-            <div className="flex items-center border border-bambu-dark-tertiary rounded-lg overflow-hidden">
+            <div className="flex items-center border border-bambu-dark-tertiary rounded-lg overflow-hidden flex-shrink-0">
               <button
                 className={`p-2 ${viewMode === 'grid' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
                 onClick={() => setViewMode('grid')}
@@ -1288,6 +1308,7 @@ export function ArchivesPage() {
                 <CalendarDays className="w-4 h-4" />
               </button>
             </div>
+            </div>
             {hasTopFilters && (
               <Button
                 variant="ghost"

+ 2 - 2
frontend/src/pages/MaintenancePage.tsx

@@ -914,7 +914,7 @@ export function MaintenancePage() {
 
   if (isLoading) {
     return (
-      <div className="p-8 flex justify-center">
+      <div className="p-4 md:p-8 flex justify-center">
         <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
       </div>
     );
@@ -924,7 +924,7 @@ export function MaintenancePage() {
   const totalWarning = overview?.reduce((sum, p) => sum + p.warning_count, 0) || 0;
 
   return (
-    <div className="p-8">
+    <div className="p-4 md:p-8">
       {/* Header */}
       <div className="mb-6">
         <h1 className="text-2xl font-bold text-white">Maintenance</h1>

+ 1 - 1
frontend/src/pages/PrintersPage.tsx

@@ -1819,7 +1819,7 @@ export function PrintersPage() {
   }, [sortBy, sortedPrinters]);
 
   return (
-    <div className="p-8">
+    <div className="p-4 md:p-8">
       <div className="flex items-center justify-between mb-6">
         <div>
           <h1 className="text-2xl font-bold text-white">Printers</h1>

+ 1 - 1
frontend/src/pages/ProfilesPage.tsx

@@ -2788,7 +2788,7 @@ export function ProfilesPage() {
 
   if (statusLoading) {
     return (
-      <div className="p-8 flex items-center justify-center min-h-[400px]">
+      <div className="p-4 md:p-8 flex items-center justify-center min-h-[400px]">
         <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
       </div>
     );

+ 3 - 3
frontend/src/pages/QueuePage.tsx

@@ -144,9 +144,9 @@ function SortableQueueItem({
           <div
             {...attributes}
             {...listeners}
-            className="flex items-center justify-center w-8 h-8 rounded-lg bg-bambu-dark cursor-grab active:cursor-grabbing hover:bg-bambu-dark-tertiary transition-colors"
+            className="flex items-center justify-center w-10 h-10 md:w-8 md:h-8 rounded-lg bg-bambu-dark cursor-grab active:cursor-grabbing hover:bg-bambu-dark-tertiary transition-colors touch-manipulation"
           >
-            <GripVertical className="w-4 h-4 text-bambu-gray" />
+            <GripVertical className="w-6 h-6 md:w-4 md:h-4 text-bambu-gray" />
           </div>
         ) : position !== undefined ? (
           <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-bambu-dark text-bambu-gray text-sm font-medium">
@@ -505,7 +505,7 @@ export function QueuePage() {
   };
 
   return (
-    <div className="p-8">
+    <div className="p-4 md:p-8">
       {/* Header */}
       <div className="flex items-center justify-between mb-8">
         <div>

+ 6 - 6
frontend/src/pages/SettingsPage.tsx

@@ -305,14 +305,14 @@ export function SettingsPage() {
 
   if (isLoading || !localSettings) {
     return (
-      <div className="p-8 flex justify-center">
+      <div className="p-4 md:p-8 flex justify-center">
         <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
       </div>
     );
   }
 
   return (
-    <div className="p-8">
+    <div className="p-4 md:p-8">
       <div className="mb-8">
         <h1 className="text-2xl font-bold text-white">Settings</h1>
         <p className="text-bambu-gray">Configure Bambuddy</p>
@@ -366,9 +366,9 @@ export function SettingsPage() {
 
       {/* General Tab */}
       {activeTab === 'general' && (
-      <div className="flex gap-8">
+      <div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
         {/* Left Column - General Settings */}
-        <div className="space-y-6 flex-1 max-w-xl">
+        <div className="space-y-6 flex-1 lg:max-w-xl">
           <Card>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">{t('settings.general')}</h2>
@@ -630,7 +630,7 @@ export function SettingsPage() {
         </div>
 
         {/* Second Column - AMS & Spoolman */}
-        <div className="space-y-6 flex-1 max-w-md">
+        <div className="space-y-6 flex-1 lg:max-w-md">
           <Card>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">AMS Display Thresholds</h2>
@@ -740,7 +740,7 @@ export function SettingsPage() {
         </div>
 
         {/* Third Column - Updates */}
-        <div className="space-y-6 flex-1 max-w-sm">
+        <div className="space-y-6 flex-1 lg:max-w-sm">
           <Card>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">Updates</h2>

+ 2 - 2
frontend/src/pages/StatsPage.tsx

@@ -338,7 +338,7 @@ export function StatsPage() {
 
   if (isLoading) {
     return (
-      <div className="p-8">
+      <div className="p-4 md:p-8">
         <div className="text-center py-12 text-bambu-gray">Loading statistics...</div>
       </div>
     );
@@ -392,7 +392,7 @@ export function StatsPage() {
   ];
 
   return (
-    <div className="p-8">
+    <div className="p-4 md:p-8">
       <div className="mb-6">
         <h1 className="text-2xl font-bold text-white">Dashboard</h1>
         <p className="text-bambu-gray">Drag widgets to rearrange. Click the eye icon to hide.</p>

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-BDGCrNHI.css


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-CcC-KyfM.css


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-DHVVjLkT.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-iXqNZkoR.js


+ 2 - 2
static/index.html

@@ -7,8 +7,8 @@
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-DHVVjLkT.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CcC-KyfM.css">
+    <script type="module" crossorigin src="/assets/index-iXqNZkoR.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BDGCrNHI.css">
   </head>
   <body>
     <div id="root"></div>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff