mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 03:31:25 +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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user