mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
* 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
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
<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`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className={`border-b border-bambu-dark-tertiary flex items-center justify-center ${sidebarExpanded ? 'p-4' : 'p-2'}`}>
|
||||
{/* 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={sidebarExpanded ? 'h-16 w-auto' : 'h-8 w-8 object-cover object-left'}
|
||||
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={`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 ${isMobile || sidebarExpanded ? 'p-4' : 'p-2'}`}>
|
||||
<img
|
||||
src={theme === 'dark' ? '/img/bambuddy_logo_dark.png' : '/img/bambuddy_logo_light.png'}
|
||||
alt="Bambuddy"
|
||||
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">
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user