mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
Merge pull request #382 from Keybored02/feature/mobile_view_fix
Fix UI clipping in mobile view
This commit is contained in:
@@ -187,8 +187,8 @@ export function AMSHistoryModal({
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6 overflow-y-auto max-h-[calc(90vh-80px)]">
|
||||
{/* Time Range & Mode Selector */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-1 rounded-lg p-1" style={{ backgroundColor: cardBg }}>
|
||||
<div className="flex items-center justify-between max-[550px]:flex-col max-[550px]:items-start max-[550px]:gap-3">
|
||||
<div className="inline-flex gap-1 rounded-lg p-1 max-w-full flex-wrap w-fit" style={{ backgroundColor: cardBg }}>
|
||||
<button
|
||||
onClick={() => setMode('humidity')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
@@ -211,7 +211,7 @@ export function AMSHistoryModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 rounded-lg p-1" style={{ backgroundColor: cardBg }}>
|
||||
<div className="inline-flex gap-1 rounded-lg p-1 max-w-full flex-wrap w-fit" style={{ backgroundColor: cardBg }}>
|
||||
{TIME_RANGES.map(range => (
|
||||
<button
|
||||
key={range.value}
|
||||
@@ -228,10 +228,10 @@ export function AMSHistoryModal({
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-4 gap-4 max-[550px]:grid-cols-2">
|
||||
{mode === 'humidity' ? (
|
||||
<>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-2" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.current', 'Current')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-2xl font-bold" style={{ color: getHumidityColor(currentHumidity) }}>
|
||||
@@ -240,19 +240,19 @@ export function AMSHistoryModal({
|
||||
<TrendIcon trend={humidityTrend} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-4" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.average', 'Average')}</p>
|
||||
<p className="text-2xl font-bold" style={{ color: textPrimary }}>
|
||||
{data?.avg_humidity != null ? `${data.avg_humidity}%` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-1" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.min', 'Min')}</p>
|
||||
<p className="text-2xl font-bold text-green-500">
|
||||
{data?.min_humidity != null ? `${data.min_humidity}%` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-3" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.max', 'Max')}</p>
|
||||
<p className="text-2xl font-bold text-red-500">
|
||||
{data?.max_humidity != null ? `${data.max_humidity}%` : '—'}
|
||||
@@ -261,7 +261,7 @@ export function AMSHistoryModal({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-2" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.current', 'Current')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-2xl font-bold" style={{ color: getTempColor(currentTemp) }}>
|
||||
@@ -270,19 +270,19 @@ export function AMSHistoryModal({
|
||||
<TrendIcon trend={tempTrend} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-4" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.average', 'Average')}</p>
|
||||
<p className="text-2xl font-bold" style={{ color: textPrimary }}>
|
||||
{data?.avg_temperature != null ? `${data.avg_temperature}°C` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-1" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.min', 'Min')}</p>
|
||||
<p className="text-2xl font-bold text-blue-500">
|
||||
{data?.min_temperature != null ? `${data.min_temperature}°C` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg p-4" style={{ backgroundColor: cardBg }}>
|
||||
<div className="rounded-lg p-4 max-[550px]:order-3" style={{ backgroundColor: cardBg }}>
|
||||
<p className="text-xs" style={{ color: textSecondary }}>{t('common.max', 'Max')}</p>
|
||||
<p className="text-2xl font-bold text-red-500">
|
||||
{data?.max_temperature != null ? `${data.max_temperature}°C` : '—'}
|
||||
|
||||
@@ -32,6 +32,7 @@ interface DashboardProps {
|
||||
widgets: DashboardWidget[];
|
||||
storageKey: string;
|
||||
columns?: number;
|
||||
stackBelow?: number;
|
||||
hideControls?: boolean;
|
||||
onResetLayout?: () => void;
|
||||
renderControls?: (controls: {
|
||||
@@ -54,6 +55,7 @@ function SortableWidget({
|
||||
component,
|
||||
isHidden,
|
||||
size,
|
||||
columnSpan,
|
||||
onToggleVisibility,
|
||||
onToggleSize,
|
||||
}: {
|
||||
@@ -62,6 +64,7 @@ function SortableWidget({
|
||||
component: ReactNode | ((size: 1 | 2 | 4) => ReactNode);
|
||||
isHidden: boolean;
|
||||
size: 1 | 2 | 4;
|
||||
columnSpan: number;
|
||||
onToggleVisibility: () => void;
|
||||
onToggleSize: () => void;
|
||||
}) {
|
||||
@@ -87,7 +90,7 @@ function SortableWidget({
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
...style,
|
||||
gridColumn: `span ${size}`,
|
||||
gridColumn: `span ${columnSpan}`,
|
||||
}}
|
||||
className={`bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary overflow-hidden ${
|
||||
isDragging ? 'ring-2 ring-bambu-green shadow-lg' : ''
|
||||
@@ -135,7 +138,7 @@ function SortableWidget({
|
||||
);
|
||||
}
|
||||
|
||||
export function Dashboard({ widgets, storageKey, columns = 4, hideControls = false, onResetLayout, renderControls }: DashboardProps) {
|
||||
export function Dashboard({ widgets, storageKey, columns = 4, stackBelow, hideControls = false, onResetLayout, renderControls }: DashboardProps) {
|
||||
// Build default sizes from widget definitions
|
||||
const getDefaultSizes = () => {
|
||||
const sizes: Record<string, 1 | 2 | 4> = {};
|
||||
@@ -169,6 +172,31 @@ export function Dashboard({ widgets, storageKey, columns = 4, hideControls = fal
|
||||
});
|
||||
|
||||
const [showHiddenPanel, setShowHiddenPanel] = useState(false);
|
||||
const [isStacked, setIsStacked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stackBelow) return undefined;
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${stackBelow}px)`);
|
||||
const handleChange = (event: MediaQueryListEvent | MediaQueryList) => {
|
||||
setIsStacked(event.matches);
|
||||
};
|
||||
handleChange(mediaQuery);
|
||||
const onChange = (event: MediaQueryListEvent) => handleChange(event);
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', onChange);
|
||||
} else {
|
||||
mediaQuery.addListener(onChange);
|
||||
}
|
||||
return () => {
|
||||
if (mediaQuery.removeEventListener) {
|
||||
mediaQuery.removeEventListener('change', onChange);
|
||||
} else {
|
||||
mediaQuery.removeListener(onChange);
|
||||
}
|
||||
};
|
||||
}, [stackBelow]);
|
||||
|
||||
const effectiveColumns = stackBelow && isStacked ? 1 : columns;
|
||||
|
||||
// Listen for toggle-hidden-panel event from parent
|
||||
useEffect(() => {
|
||||
@@ -324,21 +352,26 @@ export function Dashboard({ widgets, storageKey, columns = 4, hideControls = fal
|
||||
<div
|
||||
className="grid gap-6"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
||||
gridTemplateColumns: `repeat(${effectiveColumns}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{visibleWidgets.map((widget) => (
|
||||
<SortableWidget
|
||||
key={widget.id}
|
||||
id={widget.id}
|
||||
title={widget.title}
|
||||
component={widget.component}
|
||||
isHidden={layout.hidden.includes(widget.id)}
|
||||
size={layout.sizes[widget.id] || 2}
|
||||
onToggleVisibility={() => toggleVisibility(widget.id)}
|
||||
onToggleSize={() => toggleSize(widget.id)}
|
||||
/>
|
||||
))}
|
||||
{visibleWidgets.map((widget) => {
|
||||
const size = layout.sizes[widget.id] || 2;
|
||||
const columnSpan = Math.min(size, effectiveColumns);
|
||||
return (
|
||||
<SortableWidget
|
||||
key={widget.id}
|
||||
id={widget.id}
|
||||
title={widget.title}
|
||||
component={widget.component}
|
||||
isHidden={layout.hidden.includes(widget.id)}
|
||||
size={size}
|
||||
columnSpan={columnSpan}
|
||||
onToggleVisibility={() => toggleVisibility(widget.id)}
|
||||
onToggleSize={() => toggleSize(widget.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
@@ -157,7 +157,7 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Time Range Selector */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between max-[550px]:flex-col max-[550px]:items-start max-[550px]:gap-2">
|
||||
<h3 className="text-lg font-semibold text-white">Filament Usage Trends</h3>
|
||||
<div className="flex gap-1 bg-bambu-dark rounded-lg p-1">
|
||||
{(['7d', '30d', '90d', '365d', 'all'] as TimeRange[]).map((range) => (
|
||||
@@ -177,24 +177,30 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-3 gap-4 max-[640px]:grid-cols-1">
|
||||
<div className="bg-bambu-dark rounded-lg p-4">
|
||||
<p className="text-sm text-bambu-gray">Period Filament</p>
|
||||
<p className="text-2xl font-bold text-white">{(totalFilament / 1000).toFixed(2)}kg</p>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-bambu-gray leading-none">Period Filament</p>
|
||||
<p className="text-2xl font-bold text-white leading-none">{(totalFilament / 1000).toFixed(2)}kg</p>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray">{totalFilament.toFixed(0)}g total</p>
|
||||
</div>
|
||||
<div className="bg-bambu-dark rounded-lg p-4">
|
||||
<p className="text-sm text-bambu-gray">Period Cost</p>
|
||||
<p className="text-2xl font-bold text-white">{currency}{totalCost.toFixed(2)}</p>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-bambu-gray leading-none">Period Cost</p>
|
||||
<p className="text-2xl font-bold text-white leading-none">{currency}{totalCost.toFixed(2)}</p>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray">{totalPrints} prints</p>
|
||||
</div>
|
||||
<div className="bg-bambu-dark rounded-lg p-4">
|
||||
<p className="text-sm text-bambu-gray">Avg per Print</p>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{totalPrints > 0
|
||||
? (totalFilament / totalPrints).toFixed(0)
|
||||
: 0}g
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-bambu-gray leading-none">Avg per Print</p>
|
||||
<p className="text-2xl font-bold text-white leading-none">
|
||||
{totalPrints > 0
|
||||
? (totalFilament / totalPrints).toFixed(0)
|
||||
: 0}g
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
{currency}{totalPrints > 0 ? (totalCost / totalPrints).toFixed(2) : '0.00'} avg
|
||||
</p>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SwitchbarPopover } from './SwitchbarPopover';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api, supportApi, pendingUploadsApi } from '../api/client';
|
||||
import { getIconByName } from './IconPicker';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { useIsSidebarCompact } from '../hooks/useIsSidebarCompact';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { Card, CardHeader, CardContent } from './Card';
|
||||
@@ -72,7 +72,7 @@ export function Layout() {
|
||||
const location = useLocation();
|
||||
const { mode, toggleMode } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsMobile();
|
||||
const isSidebarCompact = useIsSidebarCompact();
|
||||
const { user, authEnabled, logout, hasPermission } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||
@@ -311,12 +311,12 @@ export function Layout() {
|
||||
localStorage.setItem('sidebarExpanded', String(sidebarExpanded));
|
||||
}, [sidebarExpanded]);
|
||||
|
||||
// Close mobile drawer on navigation
|
||||
// Close compact drawer on navigation
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
if (isSidebarCompact) {
|
||||
setMobileDrawerOpen(false);
|
||||
}
|
||||
}, [location.pathname, isMobile]);
|
||||
}, [location.pathname, isSidebarCompact]);
|
||||
|
||||
// Listen for plate detection warnings (objects on plate, print paused)
|
||||
// Only show to users with printers:control permission
|
||||
@@ -390,8 +390,8 @@ export function Layout() {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Mobile Header */}
|
||||
{isMobile && (
|
||||
{/* Compact Header */}
|
||||
{isSidebarCompact && (
|
||||
<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)}
|
||||
@@ -408,8 +408,8 @@ export function Layout() {
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Mobile Drawer Backdrop */}
|
||||
{isMobile && mobileDrawerOpen && (
|
||||
{/* Compact Drawer Backdrop */}
|
||||
{isSidebarCompact && mobileDrawerOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 z-40 transition-opacity"
|
||||
onClick={() => setMobileDrawerOpen(false)}
|
||||
@@ -419,17 +419,17 @@ export function Layout() {
|
||||
{/* Sidebar / Mobile Drawer */}
|
||||
<aside
|
||||
className={`bg-bambu-dark-secondary border-r border-bambu-dark-tertiary flex flex-col transition-all duration-300 ${
|
||||
isMobile
|
||||
isSidebarCompact
|
||||
? `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'}`}>
|
||||
<div className={`border-b border-bambu-dark-tertiary flex items-center justify-center ${isSidebarCompact || sidebarExpanded ? 'p-4' : 'p-2'}`}>
|
||||
<img
|
||||
src={mode === 'dark' ? '/img/bambuddy_logo_dark_transparent.png' : '/img/bambuddy_logo_light.png'}
|
||||
alt="Bambuddy"
|
||||
className={isMobile || sidebarExpanded ? 'h-16 w-auto' : 'h-8 w-8 object-cover object-left'}
|
||||
className={isSidebarCompact || sidebarExpanded ? 'h-16 w-auto' : 'h-8 w-8 object-cover object-left'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -467,10 +467,10 @@ export function Layout() {
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex items-center ${isMobile || sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group text-bambu-gray-light hover:bg-bambu-dark-tertiary hover:text-white`}
|
||||
title={!isMobile && !sidebarExpanded ? link.name : undefined}
|
||||
className={`flex items-center ${isSidebarCompact || sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group text-bambu-gray-light hover:bg-bambu-dark-tertiary hover:text-white`}
|
||||
title={!isSidebarCompact && !sidebarExpanded ? link.name : undefined}
|
||||
>
|
||||
{sidebarExpanded && !isMobile && (
|
||||
{sidebarExpanded && !isSidebarCompact && (
|
||||
<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 ? (
|
||||
@@ -482,21 +482,21 @@ export function Layout() {
|
||||
) : (
|
||||
LinkIcon && <LinkIcon className="w-5 h-5 flex-shrink-0" />
|
||||
)}
|
||||
{(isMobile || sidebarExpanded) && <span>{link.name}</span>}
|
||||
{(isSidebarCompact || sidebarExpanded) && <span>{link.name}</span>}
|
||||
</a>
|
||||
) : (
|
||||
<NavLink
|
||||
to={`/external/${link.id}`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center ${isMobile || sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group ${
|
||||
`flex items-center ${isSidebarCompact || 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={!isMobile && !sidebarExpanded ? link.name : undefined}
|
||||
title={!isSidebarCompact && !sidebarExpanded ? link.name : undefined}
|
||||
>
|
||||
{sidebarExpanded && !isMobile && (
|
||||
{sidebarExpanded && !isSidebarCompact && (
|
||||
<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 ? (
|
||||
@@ -508,7 +508,7 @@ export function Layout() {
|
||||
) : (
|
||||
LinkIcon && <LinkIcon className="w-5 h-5 flex-shrink-0" />
|
||||
)}
|
||||
{(isMobile || sidebarExpanded) && <span>{link.name}</span>}
|
||||
{(isSidebarCompact || sidebarExpanded) && <span>{link.name}</span>}
|
||||
</NavLink>
|
||||
)}
|
||||
</li>
|
||||
@@ -544,15 +544,15 @@ export function Layout() {
|
||||
<NavLink
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center ${isMobile || sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group ${
|
||||
`flex items-center ${isSidebarCompact || 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={!isMobile && !sidebarExpanded ? t(labelKey) : undefined}
|
||||
title={!isSidebarCompact && !sidebarExpanded ? t(labelKey) : undefined}
|
||||
>
|
||||
{sidebarExpanded && !isMobile && (
|
||||
{sidebarExpanded && !isSidebarCompact && (
|
||||
<GripVertical className="w-4 h-4 flex-shrink-0 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing -ml-1" />
|
||||
)}
|
||||
<div className="relative">
|
||||
@@ -565,7 +565,7 @@ export function Layout() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(isMobile || sidebarExpanded) && <span>{t(labelKey)}</span>}
|
||||
{(isSidebarCompact || sidebarExpanded) && <span>{t(labelKey)}</span>}
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
@@ -574,8 +574,8 @@ export function Layout() {
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Collapse toggle - hide on mobile */}
|
||||
{!isMobile && (
|
||||
{/* Collapse toggle - hide on compact sidebar */}
|
||||
{!isSidebarCompact && (
|
||||
<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"
|
||||
@@ -591,7 +591,7 @@ export function Layout() {
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-2 border-t border-bambu-dark-tertiary">
|
||||
{isMobile || sidebarExpanded ? (
|
||||
{isSidebarCompact || sidebarExpanded ? (
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
{/* Top row: icons */}
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
@@ -783,7 +783,7 @@ export function Layout() {
|
||||
|
||||
{/* Main content */}
|
||||
<main className={`flex-1 bg-bambu-dark overflow-auto transition-all duration-300 ${
|
||||
isMobile ? 'mt-14' : sidebarExpanded ? 'ml-64' : 'ml-16'
|
||||
isSidebarCompact ? 'mt-14' : sidebarExpanded ? 'ml-64' : 'ml-16'
|
||||
}`}>
|
||||
{/* Debug logging indicator */}
|
||||
{debugLoggingState?.enabled && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const MOBILE_BREAKPOINT = 768; // md breakpoint
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile(): boolean {
|
||||
const [isMobile, setIsMobile] = useState(() =>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const SIDEBAR_COMPACT_BREAKPOINT = 1144;
|
||||
|
||||
export function useIsSidebarCompact(): boolean {
|
||||
const [isCompact, setIsCompact] = useState(() =>
|
||||
typeof window !== 'undefined' ? window.innerWidth < SIDEBAR_COMPACT_BREAKPOINT : false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${SIDEBAR_COMPACT_BREAKPOINT - 1}px)`);
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
setIsCompact(e.matches);
|
||||
};
|
||||
|
||||
setIsCompact(mediaQuery.matches);
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
return isCompact;
|
||||
}
|
||||
@@ -173,6 +173,22 @@
|
||||
--border-color: #2a3d30;
|
||||
}
|
||||
|
||||
/* Printer card control buttons: stack only when they clip */
|
||||
.printer-control-buttons-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 220px) {
|
||||
.printer-control-buttons {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.printer-control-buttons > button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LAYER 2: STYLE EFFECTS
|
||||
============================================ */
|
||||
|
||||
@@ -286,9 +286,9 @@ function MaintenanceCard({
|
||||
|
||||
return (
|
||||
<div className={`rounded-xl border p-4 transition-all ${getBgColor()}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-start gap-3 max-[550px]:flex-wrap">
|
||||
{/* Icon with status indicator */}
|
||||
<div className={`relative p-2.5 rounded-lg ${
|
||||
<div className={`relative p-2.5 rounded-lg shrink-0 ${
|
||||
item.is_due ? 'bg-red-500/20' :
|
||||
item.is_warning ? 'bg-amber-500/20' :
|
||||
item.enabled ? 'bg-bambu-dark' : 'bg-bambu-dark/50'
|
||||
@@ -351,7 +351,7 @@ function MaintenanceCard({
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0 max-[550px]:w-full max-[550px]:justify-end max-[550px]:mt-1">
|
||||
<span title={!hasPermission('maintenance:update') ? t('maintenance.noPermissionUpdate') : undefined}>
|
||||
<Toggle
|
||||
checked={item.enabled}
|
||||
|
||||
@@ -1309,7 +1309,7 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
if (!printers?.length) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-4 gap-y-2 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2 h-2 rounded-full ${counts.idle > 0 ? 'bg-bambu-green' : 'bg-gray-500'}`} />
|
||||
<span className="text-bambu-gray">
|
||||
@@ -1335,17 +1335,21 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
{nextFinish && (
|
||||
<>
|
||||
<div className="w-px h-4 bg-bambu-dark-tertiary" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-bambu-green font-medium">{t('printers.nextAvailable')}:</span>
|
||||
<span className="text-white font-medium">{nextFinish.name}</span>
|
||||
<div className="w-16 bg-bambu-dark-tertiary rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-bambu-green h-1.5 rounded-full transition-all"
|
||||
style={{ width: `${nextFinish.progress}%` }}
|
||||
/>
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-bambu-green font-medium">{t('printers.nextAvailable')}:</span>
|
||||
<span className="text-white font-medium">{nextFinish.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
<div className="w-full sm:w-16 bg-bambu-dark-tertiary rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-bambu-green h-1.5 rounded-full transition-all"
|
||||
style={{ width: `${nextFinish.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-white font-medium">{Math.round(nextFinish.progress)}%</span>
|
||||
<span className="text-bambu-gray">({formatTime(nextFinish.remainingMin * 60)})</span>
|
||||
</div>
|
||||
<span className="text-white font-medium">{Math.round(nextFinish.progress)}%</span>
|
||||
<span className="text-bambu-gray">({formatTime(nextFinish.remainingMin * 60)})</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -2622,9 +2626,9 @@ function PrinterCard({
|
||||
<div className="flex-1 h-px bg-bambu-dark-tertiary/30" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center justify-between gap-2 max-[550px]:items-start">
|
||||
{/* Left: Fan Status - always visible, dynamic coloring */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0 max-[550px]:flex-wrap max-[550px]:items-start max-[550px]:gap-1.5">
|
||||
{/* Part Cooling Fan */}
|
||||
<div
|
||||
className={`flex items-center gap-1 px-1.5 py-1 rounded ${partFan && partFan > 0 ? 'bg-cyan-500/10' : 'bg-bambu-dark'}`}
|
||||
@@ -2660,7 +2664,7 @@ function PrinterCard({
|
||||
</div>
|
||||
|
||||
{/* Right: Print Control Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-shrink-0 max-[550px]:self-start">
|
||||
{/* Stop button */}
|
||||
<button
|
||||
onClick={() => setShowStopConfirm(true)}
|
||||
@@ -2746,7 +2750,7 @@ function PrinterCard({
|
||||
)}
|
||||
</div>
|
||||
{(ams.humidity != null || ams.temp != null) && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 max-[550px]:flex-col max-[550px]:items-start">
|
||||
{ams.humidity != null && (
|
||||
<HumidityIndicator
|
||||
humidity={ams.humidity}
|
||||
@@ -3100,9 +3104,9 @@ function PrinterCard({
|
||||
)}
|
||||
</div>
|
||||
{/* Row 2: Slot (left) + Stats (right stacked) */}
|
||||
<div className="flex gap-1.5">
|
||||
<div className="flex gap-1.5 max-[550px]:flex-col max-[550px]:items-start">
|
||||
{/* Slot wrapper with menu button, dropdown, and loading overlay */}
|
||||
<div className="relative group flex-1">
|
||||
<div className="relative group flex-1 max-[550px]:w-full">
|
||||
{/* Loading overlay during RFID re-read */}
|
||||
{isHtRefreshing && (
|
||||
<div className="absolute inset-0 bg-bambu-dark-tertiary/80 rounded flex items-center justify-center z-20">
|
||||
@@ -3226,7 +3230,7 @@ function PrinterCard({
|
||||
</div>
|
||||
{/* Stats stacked vertically: Temp on top, Humidity below */}
|
||||
{(ams.humidity != null || ams.temp != null) && (
|
||||
<div className="flex flex-col justify-center gap-1 shrink-0">
|
||||
<div className="flex flex-col justify-center gap-1 shrink-0 max-[550px]:w-full">
|
||||
{ams.temp != null && (
|
||||
<TemperatureIndicator
|
||||
temp={ams.temp}
|
||||
|
||||
@@ -262,7 +262,7 @@ function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 max-[550px]:flex-wrap max-[550px]:items-center">
|
||||
{step === 'code' && (
|
||||
<Button type="button" variant="secondary" onClick={() => setStep('email')} className="flex-1">
|
||||
{t('profiles.login.back')}
|
||||
@@ -1723,7 +1723,7 @@ function CreatePresetModal({
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card className="w-full max-w-6xl max-h-[90vh] flex flex-col">
|
||||
<Card className="w-full max-w-6xl max-h-[90vh] flex flex-col overflow-y-auto">
|
||||
<CardContent className="p-0 flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
@@ -1764,7 +1764,7 @@ function CreatePresetModal({
|
||||
|
||||
{/* Basic Info */}
|
||||
<div className="p-4 border-b border-bambu-dark-tertiary space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-3 gap-4 max-[640px]:grid-cols-1">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('common.type')}</label>
|
||||
<select
|
||||
@@ -1815,7 +1815,7 @@ function CreatePresetModal({
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-bambu-dark-tertiary">
|
||||
<div className="flex border-b border-bambu-dark-tertiary max-[640px]:flex-wrap max-[640px]:items-center">
|
||||
<button
|
||||
onClick={() => setActiveTab('common')}
|
||||
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
@@ -1844,7 +1844,7 @@ function CreatePresetModal({
|
||||
JSON
|
||||
{jsonError && <AlertCircle className="w-3 h-3 text-red-400" />}
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<div className="flex-1 max-[640px]:hidden" />
|
||||
<button
|
||||
onClick={() => {
|
||||
const exportData = {
|
||||
@@ -1904,7 +1904,7 @@ function CreatePresetModal({
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="flex-1 p-4">
|
||||
{activeTab === 'common' && (
|
||||
<div className="space-y-6">
|
||||
{/* Templates */}
|
||||
@@ -2010,9 +2010,9 @@ function CreatePresetModal({
|
||||
<h3 className="text-sm font-medium text-white mb-3">{t('profiles.presets.commonSettings')}</h3>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
|
||||
{dynamicFields.slice(0, 10).map(field => (
|
||||
<div key={field.key} className="flex items-center justify-between gap-4">
|
||||
<div key={field.key} className="flex items-center justify-between gap-4 max-[640px]:flex-col max-[640px]:items-start">
|
||||
<label className="text-sm text-bambu-gray flex-shrink-0">{field.label}</label>
|
||||
<div className="w-48">{renderFieldInput(field)}</div>
|
||||
<div className="w-48 max-[640px]:w-full">{renderFieldInput(field)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -2460,7 +2460,7 @@ function CloudProfilesView({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 max-[550px]:flex-wrap max-[550px]:items-center">
|
||||
<Button
|
||||
variant={compareMode ? 'primary' : 'secondary'}
|
||||
onClick={() => {
|
||||
|
||||
@@ -760,6 +760,7 @@ export function StatsPage() {
|
||||
key={dashboardKey}
|
||||
widgets={widgets}
|
||||
storageKey="bambusy-dashboard-layout"
|
||||
stackBelow={640}
|
||||
hideControls
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user