Matteo Parenti 6 месяцев назад
Родитель
Сommit
b6c304d4a9

+ 39 - 4
frontend/src/components/Dashboard.tsx

@@ -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('matches' in event ? event.matches : 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,20 +352,27 @@ 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) => (
+              (() => {
+                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={layout.sizes[widget.id] || 2}
+                size={size}
+                columnSpan={columnSpan}
                 onToggleVisibility={() => toggleVisibility(widget.id)}
                 onToggleSize={() => toggleSize(widget.id)}
               />
+                );
+              })()
             ))}
           </div>
         </SortableContext>

+ 18 - 12
frontend/src/components/FilamentTrends.tsx

@@ -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-[546px]:flex-col max-[546px]:items-start max-[546px]: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-[600px]: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>

+ 16 - 0
frontend/src/index.css

@@ -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
frontend/src/pages/MaintenancePage.tsx

@@ -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
frontend/src/pages/PrintersPage.tsx

@@ -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>
         </>
       )}
@@ -2575,9 +2579,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-[480px]: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-[480px]:flex-wrap max-[480px]:items-start max-[480px]: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'}`}
@@ -2613,7 +2617,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-[480px]:self-start">
                       {/* Stop button */}
                       <button
                         onClick={() => setShowStopConfirm(true)}
@@ -2699,7 +2703,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-[480px]:flex-col max-[480px]:items-start">
                                   {ams.humidity != null && (
                                     <HumidityIndicator
                                       humidity={ams.humidity}
@@ -3049,9 +3053,9 @@ function PrinterCard({
                               )}
                             </div>
                             {/* Row 2: Slot (left) + Stats (right stacked) */}
-                            <div className="flex gap-1.5">
+                            <div className="flex gap-1.5 max-[480px]:flex-col max-[480px]:items-start">
                               {/* Slot wrapper with menu button, dropdown, and loading overlay */}
-                              <div className="relative group flex-1">
+                              <div className="relative group flex-1 max-[480px]: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">
@@ -3171,7 +3175,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-[480px]:w-full">
                                   {ams.temp != null && (
                                     <TemperatureIndicator
                                       temp={ams.temp}

+ 9 - 9
frontend/src/pages/ProfilesPage.tsx

@@ -262,7 +262,7 @@ function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) {
             </div>
           )}
 
-          <div className="flex gap-2">
+          <div className="flex gap-2 max-[510px]:flex-wrap max-[510px]: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-[565px]:flex-wrap max-[565px]: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-[565px]: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-[633px]:flex-col max-[633px]: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-[633px]: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-[510px]:flex-wrap max-[510px]:items-center">
             <Button
               variant={compareMode ? 'primary' : 'secondary'}
               onClick={() => {

+ 1 - 0
frontend/src/pages/StatsPage.tsx

@@ -760,6 +760,7 @@ export function StatsPage() {
         key={dashboardKey}
         widgets={widgets}
         storageKey="bambusy-dashboard-layout"
+        stackBelow={766}
         hideControls
       />
     </div>