Update responsive breakpoints for various components, implement useIsSidebarCompact hook

This commit is contained in:
Matteo Parenti
2026-02-15 14:24:50 +01:00
parent 66dcca50f8
commit 4ff3e2a60f
10 changed files with 98 additions and 76 deletions
+10 -10
View File
@@ -187,7 +187,7 @@ 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 max-[511px]:flex-col max-[511px]:items-start max-[511px]:gap-3">
<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')}
@@ -228,10 +228,10 @@ export function AMSHistoryModal({
</div>
{/* Stats Cards */}
<div className="grid grid-cols-4 gap-4 max-[511px]:grid-cols-2">
<div className="grid grid-cols-4 gap-4 max-[550px]:grid-cols-2">
{mode === 'humidity' ? (
<>
<div className="rounded-lg p-4 max-[511px]:order-2" 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 max-[511px]:order-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 max-[511px]:order-1" 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 max-[511px]:order-3" 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 max-[511px]:order-2" 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 max-[511px]:order-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 max-[511px]:order-1" 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 max-[511px]:order-3" 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` : '—'}
+18 -20
View File
@@ -178,7 +178,7 @@ export function Dashboard({ widgets, storageKey, columns = 4, stackBelow, hideCo
if (!stackBelow) return undefined;
const mediaQuery = window.matchMedia(`(max-width: ${stackBelow}px)`);
const handleChange = (event: MediaQueryListEvent | MediaQueryList) => {
setIsStacked('matches' in event ? event.matches : event.matches);
setIsStacked(event.matches);
};
handleChange(mediaQuery);
const onChange = (event: MediaQueryListEvent) => handleChange(event);
@@ -355,25 +355,23 @@ export function Dashboard({ widgets, storageKey, columns = 4, stackBelow, hideCo
gridTemplateColumns: `repeat(${effectiveColumns}, minmax(0, 1fr))`,
}}
>
{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)}
/>
);
})()
))}
{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>
+2 -2
View File
@@ -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 max-[546px]:flex-col max-[546px]:items-start max-[546px]:gap-2">
<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,7 +177,7 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
</div>
{/* Summary Cards */}
<div className="grid grid-cols-3 gap-4 max-[600px]:grid-cols-1">
<div className="grid grid-cols-3 gap-4 max-[640px]:grid-cols-1">
<div className="bg-bambu-dark rounded-lg p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-bambu-gray leading-none">Period Filament</p>
+28 -28
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
const MOBILE_BREAKPOINT = 1144; // hide sidebar below this width
const MOBILE_BREAKPOINT = 768;
export function useIsMobile(): boolean {
const [isMobile, setIsMobile] = useState(() =>
+24
View File
@@ -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;
}
+1 -1
View File
@@ -351,7 +351,7 @@ function MaintenanceCard({
</div>
{/* Actions */}
<div className="flex items-center gap-2 shrink-0 max-[550px]:w-full max-[550px]:justify-end max-[550px]:mt+1">
<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}
+7 -7
View File
@@ -2626,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 max-[480px]:items-start">
<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 min-w-0 max-[480px]:flex-wrap max-[480px]:items-start max-[480px]:gap-1.5">
<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'}`}
@@ -2664,7 +2664,7 @@ function PrinterCard({
</div>
{/* Right: Print Control Buttons */}
<div className="flex items-center gap-2 flex-shrink-0 max-[480px]:self-start">
<div className="flex items-center gap-2 flex-shrink-0 max-[550px]:self-start">
{/* Stop button */}
<button
onClick={() => setShowStopConfirm(true)}
@@ -2750,7 +2750,7 @@ function PrinterCard({
)}
</div>
{(ams.humidity != null || ams.temp != null) && (
<div className="flex items-center gap-1.5 max-[480px]:flex-col max-[480px]:items-start">
<div className="flex items-center gap-1.5 max-[550px]:flex-col max-[550px]:items-start">
{ams.humidity != null && (
<HumidityIndicator
humidity={ams.humidity}
@@ -3104,9 +3104,9 @@ function PrinterCard({
)}
</div>
{/* Row 2: Slot (left) + Stats (right stacked) */}
<div className="flex gap-1.5 max-[480px]:flex-col max-[480px]:items-start">
<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 max-[480px]:w-full">
<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">
@@ -3230,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 max-[480px]:w-full">
<div className="flex flex-col justify-center gap-1 shrink-0 max-[550px]:w-full">
{ams.temp != null && (
<TemperatureIndicator
temp={ams.temp}
+6 -6
View File
@@ -262,7 +262,7 @@ function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) {
</div>
)}
<div className="flex gap-2 max-[510px]:flex-wrap max-[510px]:items-center">
<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')}
@@ -1815,7 +1815,7 @@ function CreatePresetModal({
</div>
{/* Tabs */}
<div className="flex border-b border-bambu-dark-tertiary max-[565px]:flex-wrap max-[565px]:items-center">
<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 max-[565px]:hidden" />
<div className="flex-1 max-[640px]:hidden" />
<button
onClick={() => {
const exportData = {
@@ -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 max-[633px]:flex-col max-[633px]:items-start">
<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 max-[633px]:w-full">{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 max-[510px]:flex-wrap max-[510px]:items-center">
<div className="flex gap-2 max-[550px]:flex-wrap max-[550px]:items-center">
<Button
variant={compareMode ? 'primary' : 'secondary'}
onClick={() => {
+1 -1
View File
@@ -760,7 +760,7 @@ export function StatsPage() {
key={dashboardKey}
widgets={widgets}
storageKey="bambusy-dashboard-layout"
stackBelow={766}
stackBelow={640}
hideControls
/>
</div>