Merge branch '0.2.0b' into feature/storage_meter

This commit is contained in:
Keybored
2026-02-15 14:33:52 +01:00
committed by GitHub
11 changed files with 182 additions and 98 deletions
+12 -12
View File
@@ -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` : '—'}
+48 -15
View File
@@ -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>
+18 -12
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">
<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>
+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 = 768; // md breakpoint
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;
}
+16
View File
@@ -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
============================================ */
+3 -3
View File
@@ -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}
+22 -18
View File
@@ -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}
+9 -9
View File
@@ -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={() => {
+1
View File
@@ -760,6 +760,7 @@ export function StatsPage() {
key={dashboardKey}
widgets={widgets}
storageKey="bambusy-dashboard-layout"
stackBelow={640}
hideControls
/>
</div>