Control page: new filament path wiring

This commit is contained in:
Martin Ziegler
2025-12-02 16:39:37 +01:00
parent 088b40cefa
commit 863dedf6f4
7 changed files with 383 additions and 219 deletions
@@ -39,6 +39,7 @@ interface AMSPanelContentProps {
onSelectTray: (trayId: number | null) => void;
}
// Panel content - NO wiring, just slots and info
function AMSPanelContent({
units,
side,
@@ -52,12 +53,12 @@ function AMSPanelContent({
const slotPrefix = side === 'left' ? 'A' : 'B';
return (
<div className="flex-1 min-w-0 overflow-visible">
<div className="flex-1 min-w-0">
<div className="text-center text-[11px] font-semibold text-bambu-gray uppercase mb-2">
{side === 'left' ? 'Left Nozzle' : 'Right Nozzle'}
</div>
{/* AMS Tab Selectors - only show connected units */}
{/* AMS Tab Selectors */}
<div className="flex gap-1.5 mb-2.5 p-1.5 bg-bambu-dark/50 rounded-lg w-fit">
{units.map((unit, index) => (
<button
@@ -86,7 +87,7 @@ function AMSPanelContent({
{/* AMS Content */}
{selectedUnit && (
<div className="bg-bambu-dark-secondary rounded-[10px] p-2.5 pb-0 overflow-visible">
<div className="bg-bambu-dark-secondary rounded-[10px] p-2.5">
{/* AMS Header - Humidity & Temp */}
<div className="flex items-center gap-2.5 text-xs text-bambu-gray mb-2">
{selectedUnit.humidity !== null && (
@@ -117,8 +118,8 @@ function AMSPanelContent({
))}
</div>
{/* AMS Slots with integrated wiring */}
<div className="flex justify-center gap-1.5 mb-0">
{/* AMS Slots - NO wiring here */}
<div className="flex justify-center gap-1.5">
{selectedUnit.tray.map((tray) => {
const globalTrayId = selectedUnit.id * 4 + tray.id;
const isSelected = selectedTray === globalTrayId;
@@ -126,67 +127,42 @@ function AMSPanelContent({
const isLight = isLightColor(tray.tray_color);
return (
<div key={tray.id} className="flex flex-col items-center">
<button
onClick={() => !isEmpty && onSelectTray(isSelected ? null : globalTrayId)}
disabled={isEmpty || isPrinting}
className={`w-12 h-[70px] rounded-md border-2 overflow-hidden transition-all bg-bambu-dark ${
isSelected
? 'border-[#d4a84b]'
: 'border-bambu-dark-tertiary hover:border-bambu-gray'
} ${isEmpty ? 'opacity-50' : ''} disabled:cursor-not-allowed`}
<button
key={tray.id}
onClick={() => !isEmpty && onSelectTray(isSelected ? null : globalTrayId)}
disabled={isEmpty || isPrinting}
className={`w-12 h-[70px] rounded-md border-2 overflow-hidden transition-all bg-bambu-dark ${
isSelected
? 'border-[#d4a84b]'
: 'border-bambu-dark-tertiary hover:border-bambu-gray'
} ${isEmpty ? 'opacity-50' : ''} disabled:cursor-not-allowed`}
>
<div
className="w-full h-full flex flex-col items-center justify-end pb-[5px]"
style={{
backgroundColor: isEmpty ? undefined : hexToRgb(tray.tray_color),
}}
>
<div
className="w-full h-full flex flex-col items-center justify-end pb-[5px]"
style={{
backgroundColor: isEmpty ? undefined : hexToRgb(tray.tray_color),
}}
<span
className={`text-[11px] font-semibold mb-1 ${
isLight ? 'text-gray-800' : 'text-white'
} ${isLight ? '' : 'drop-shadow-sm'}`}
>
<span
className={`text-[11px] font-semibold mb-1 ${
isLight ? 'text-gray-800' : 'text-white'
} ${isLight ? '' : 'drop-shadow-sm'}`}
>
{isEmpty ? '--' : tray.tray_type}
</span>
{!isEmpty && (
<img
src="/icons/eye.svg"
alt=""
className={`w-3.5 h-3.5 ${isLight ? '' : 'invert'}`}
style={{ opacity: 0.8 }}
/>
)}
</div>
</button>
{/* Vertical wire from slot center down */}
<div className="w-[2px] h-[14px] bg-[#909090]" />
</div>
{isEmpty ? '--' : tray.tray_type}
</span>
{!isEmpty && (
<img
src="/icons/eye.svg"
alt=""
className={`w-3.5 h-3.5 ${isLight ? '' : 'invert'}`}
style={{ opacity: 0.8 }}
/>
)}
</div>
</button>
);
})}
</div>
{/* Wiring visualization - horizontal bar and hub */}
<div className="flex justify-center">
<div className="relative h-[50px]" style={{ width: '210px' }}>
{/* Horizontal bar connecting all slots (spans from first to last slot center) */}
<div className="absolute left-[24px] right-[24px] top-0 border-t-2 border-[#909090]" />
{/* Center hub box on the horizontal bar */}
<div className="absolute left-1/2 -translate-x-1/2 top-[-6px] w-[28px] h-[14px] bg-[#c0c0c0] border border-[#909090] rounded-sm" />
{/* Vertical wire from hub going down */}
<div className="absolute left-1/2 -translate-x-[1px] top-[8px] h-[14px] border-l-2 border-[#909090]" />
{/* Horizontal wire from hub toward the center of the panel (extends beyond panel edge) */}
{side === 'left' && (
<div className="absolute left-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
)}
{side === 'right' && (
<div className="absolute right-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
)}
</div>
</div>
</div>
)}
@@ -200,14 +176,88 @@ function AMSPanelContent({
);
}
// Unified wiring layer - draws ALL wiring in one place
interface WiringLayerProps {
isDualNozzle: boolean;
}
function WiringLayer({ isDualNozzle }: WiringLayerProps) {
if (!isDualNozzle) return null;
// All measurements relative to this container
// Container spans full width between panels
// Left panel wiring: slots → hub → down → right → down to extruder
// Right panel wiring: slots → hub → down → left → down to extruder
return (
<div className="relative w-full" style={{ height: '120px' }}>
{/* SVG for all wiring - single coordinate system */}
<svg
className="absolute inset-0 w-full h-full"
viewBox="0 0 600 120"
preserveAspectRatio="xMidYMid meet"
>
{/* Left panel wiring */}
{/* Vertical lines from 4 slots */}
<line x1="68" y1="0" x2="68" y2="14" stroke="#909090" strokeWidth="2" />
<line x1="122" y1="0" x2="122" y2="14" stroke="#909090" strokeWidth="2" />
<line x1="176" y1="0" x2="176" y2="14" stroke="#909090" strokeWidth="2" />
<line x1="230" y1="0" x2="230" y2="14" stroke="#909090" strokeWidth="2" />
{/* Horizontal bar connecting left slots */}
<line x1="68" y1="14" x2="230" y2="14" stroke="#909090" strokeWidth="2" />
{/* Left hub */}
<rect x="135" y="8" width="28" height="14" rx="2" fill="#c0c0c0" stroke="#909090" strokeWidth="1" />
{/* Vertical from left hub down */}
<line x1="149" y1="22" x2="149" y2="36" stroke="#909090" strokeWidth="2" />
{/* Horizontal from left hub toward center */}
<line x1="149" y1="36" x2="288" y2="36" stroke="#909090" strokeWidth="2" />
{/* Vertical down to left extruder inlet */}
<line x1="288" y1="36" x2="288" y2="85" stroke="#909090" strokeWidth="2" />
{/* Right panel wiring */}
{/* Vertical lines from 4 slots */}
<line x1="370" y1="0" x2="370" y2="14" stroke="#909090" strokeWidth="2" />
<line x1="424" y1="0" x2="424" y2="14" stroke="#909090" strokeWidth="2" />
<line x1="478" y1="0" x2="478" y2="14" stroke="#909090" strokeWidth="2" />
<line x1="532" y1="0" x2="532" y2="14" stroke="#909090" strokeWidth="2" />
{/* Horizontal bar connecting right slots */}
<line x1="370" y1="14" x2="532" y2="14" stroke="#909090" strokeWidth="2" />
{/* Right hub */}
<rect x="437" y="8" width="28" height="14" rx="2" fill="#c0c0c0" stroke="#909090" strokeWidth="1" />
{/* Vertical from right hub down */}
<line x1="451" y1="22" x2="451" y2="36" stroke="#909090" strokeWidth="2" />
{/* Horizontal from right hub toward center */}
<line x1="312" y1="36" x2="451" y2="36" stroke="#909090" strokeWidth="2" />
{/* Vertical down to right extruder inlet */}
<line x1="312" y1="36" x2="312" y2="85" stroke="#909090" strokeWidth="2" />
</svg>
{/* Extruder image - positioned at bottom center */}
<img
src="/icons/extruder-left-right.png"
alt="Extruder"
className="absolute left-1/2 -translate-x-1/2 bottom-0 h-[50px]"
/>
</div>
);
}
export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDualProps) {
const isConnected = status?.connected ?? false;
const isPrinting = status?.state === 'RUNNING';
const isDualNozzle = nozzleCount > 1;
const amsUnits: AMSUnit[] = status?.ams ?? [];
// For dual nozzle, split AMS units between left and right
// In real implementation, this would be based on actual nozzle assignment
const leftUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 0) : amsUnits;
const rightUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 1) : [];
@@ -236,30 +286,9 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
const isLoading = loadMutation.isPending || unloadMutation.isPending;
return (
<div className="bg-bambu-dark-tertiary rounded-[10px] p-3 relative overflow-visible">
{/* Center wiring and Extruder - absolutely centered between the two AMS panels */}
{isDualNozzle && (
<>
{/* Center wiring: two vertical lines going down to extruder inlets */}
{/* Positioned to connect with horizontal wires from AMS panels */}
<div className="absolute left-1/2 -translate-x-1/2 bottom-[62px] pointer-events-none" style={{ width: '24px', height: '30px' }}>
{/* Left vertical line - connects to left AMS horizontal wire, goes to left extruder inlet */}
<div className="absolute left-0 top-0 h-full border-l-2 border-[#909090]" />
{/* Right vertical line - connects to right AMS horizontal wire, goes to right extruder inlet */}
<div className="absolute right-0 top-0 h-full border-l-2 border-[#909090]" />
</div>
{/* Extruder */}
<img
src="/icons/extruder-left-right.png"
alt="Extruder"
className="absolute h-[50px] left-1/2 -translate-x-1/2 bottom-[12px]"
/>
</>
)}
{/* Dual Panel Layout */}
<div className="flex gap-5 overflow-visible">
{/* Left Nozzle Panel */}
<div className="bg-bambu-dark-tertiary rounded-[10px] p-3">
{/* Dual Panel Layout - just the panels, no wiring */}
<div className="flex gap-5">
<AMSPanelContent
units={leftUnits}
side="left"
@@ -270,7 +299,6 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
onSelectTray={setSelectedTray}
/>
{/* Right Nozzle Panel - only for dual nozzle */}
{isDualNozzle && (
<AMSPanelContent
units={rightUnits}
@@ -284,9 +312,11 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
)}
</div>
{/* Action Buttons Row with Extruder */}
<div className="flex items-start pt-2">
{/* Left buttons */}
{/* Unified Wiring Layer - ALL wiring drawn here */}
<WiringLayer isDualNozzle={isDualNozzle} />
{/* Action Buttons Row - aligned with extruder */}
<div className="flex items-start -mt-[50px]">
<div className="flex items-center gap-2">
<button className="w-10 h-10 rounded-lg bg-bambu-dark-secondary hover:bg-bambu-dark border border-bambu-dark-tertiary flex items-center justify-center">
<img src="/icons/ams-settings.svg" alt="Settings" className="w-5 icon-theme" />
@@ -296,10 +326,8 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
</button>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Right buttons */}
<div className="flex items-center gap-2">
<button
onClick={handleUnload}
@@ -1,7 +1,9 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { api, isConfirmationRequired } from '../../api/client';
import type { PrinterStatus } from '../../api/client';
import { ChevronUp, ChevronDown } from 'lucide-react';
import { ConfirmModal } from '../ConfirmModal';
interface ExtruderControlsProps {
printerId: number;
@@ -9,18 +11,34 @@ interface ExtruderControlsProps {
nozzleCount: number;
}
export function ExtruderControls({ status, nozzleCount }: ExtruderControlsProps) {
export function ExtruderControls({ printerId, status, nozzleCount }: ExtruderControlsProps) {
const isConnected = status?.connected ?? false;
const isPrinting = status?.state === 'RUNNING' || status?.state === 'PAUSE';
const isDualNozzle = nozzleCount > 1;
const [selectedNozzle, setSelectedNozzle] = useState<'left' | 'right'>('left');
const [confirmModal, setConfirmModal] = useState<{
token: string;
warning: string;
distance: number;
} | null>(null);
// TODO: Add extrude/retract API calls when available
const extrudeMutation = useMutation({
mutationFn: async ({ distance }: { distance: number }) => {
// Placeholder - implement when API is ready
console.log(`Extrude ${distance}mm on ${selectedNozzle} nozzle`);
mutationFn: ({ distance, token }: { distance: number; token?: string }) => {
// G-code for extrusion: relative mode, extrude, back to absolute
// T0/T1 selects the tool for dual nozzle
const toolSelect = isDualNozzle ? `T${selectedNozzle === 'left' ? 0 : 1}\n` : '';
const gcode = `${toolSelect}G91\nG1 E${distance} F300\nG90`;
return api.sendGcode(printerId, gcode, token);
},
onSuccess: (result, variables) => {
if (isConfirmationRequired(result)) {
setConfirmModal({
token: result.token,
warning: result.warning,
distance: variables.distance,
});
}
},
});
@@ -28,71 +46,91 @@ export function ExtruderControls({ status, nozzleCount }: ExtruderControlsProps)
extrudeMutation.mutate({ distance });
};
const handleConfirm = () => {
if (confirmModal) {
extrudeMutation.mutate({ distance: confirmModal.distance, token: confirmModal.token });
setConfirmModal(null);
}
};
const isDisabled = !isConnected || isPrinting || extrudeMutation.isPending;
return (
<div className="flex flex-col items-center gap-1.5 justify-center">
{/* Left/Right Toggle - only for dual nozzle */}
{isDualNozzle && (
<div className="flex rounded-md overflow-hidden border border-bambu-dark-tertiary mb-1 flex-shrink-0">
<button
onClick={() => setSelectedNozzle('left')}
className={`px-3 py-1.5 text-sm border-r border-bambu-dark-tertiary transition-colors ${
selectedNozzle === 'left'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
}`}
>
Left
</button>
<button
onClick={() => setSelectedNozzle('right')}
className={`px-3 py-1.5 text-sm transition-colors ${
selectedNozzle === 'right'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
}`}
>
Right
</button>
<>
<div className="flex flex-col items-center gap-1.5 justify-center">
{/* Left/Right Toggle - only for dual nozzle */}
{isDualNozzle && (
<div className="flex rounded-md overflow-hidden border border-bambu-dark-tertiary mb-1 flex-shrink-0">
<button
onClick={() => setSelectedNozzle('left')}
className={`px-3 py-1.5 text-sm border-r border-bambu-dark-tertiary transition-colors ${
selectedNozzle === 'left'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
}`}
>
Left
</button>
<button
onClick={() => setSelectedNozzle('right')}
className={`px-3 py-1.5 text-sm transition-colors ${
selectedNozzle === 'right'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
}`}
>
Right
</button>
</div>
)}
{/* Extrude Up Button */}
<button
onClick={() => handleExtrude(5)}
disabled={isDisabled}
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
title="Extrude 5mm"
>
<ChevronUp className="w-4 h-4" />
</button>
{/* Extruder Image */}
<div className="h-[120px] flex items-center justify-center">
<img
src={isDualNozzle ? "/icons/dual-extruder.png" : "/icons/single-extruder1.png"}
alt={isDualNozzle ? "Dual Extruder" : "Single Extruder"}
className="h-full object-contain"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
)}
{/* Extrude Up Button */}
<button
onClick={() => handleExtrude(5)}
disabled={isDisabled}
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
title="Extrude 5mm"
>
<ChevronUp className="w-4 h-4" />
</button>
{/* Retract Down Button */}
<button
onClick={() => handleExtrude(-5)}
disabled={isDisabled}
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
title="Retract 5mm"
>
<ChevronDown className="w-4 h-4" />
</button>
{/* Extruder Image */}
<div className="h-[120px] flex items-center justify-center">
<img
src={isDualNozzle ? "/icons/dual-extruder.png" : "/icons/single-extruder1.png"}
alt={isDualNozzle ? "Dual Extruder" : "Single Extruder"}
className="h-full object-contain"
onError={(e) => {
// Fallback if image doesn't load
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
{/* Label */}
<span className="text-xs text-bambu-gray mt-0.5">Extruder</span>
</div>
{/* Retract Down Button */}
<button
onClick={() => handleExtrude(-5)}
disabled={isDisabled}
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
title="Retract 5mm"
>
<ChevronDown className="w-4 h-4" />
</button>
{/* Label */}
<span className="text-xs text-bambu-gray mt-0.5">Extruder</span>
</div>
{/* Confirmation Modal */}
{confirmModal && (
<ConfirmModal
title="Confirm Extrusion"
message={confirmModal.warning}
confirmText="Continue"
variant="warning"
onConfirm={handleConfirm}
onCancel={() => setConfirmModal(null)}
/>
)}
</>
);
}
@@ -1,3 +1,6 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { api } from '../../api/client';
import type { PrinterStatus } from '../../api/client';
interface Temperatures {
@@ -11,14 +14,102 @@ interface Temperatures {
}
interface TemperatureColumnProps {
printerId?: number;
printerId: number;
status: PrinterStatus | null | undefined;
nozzleCount: number;
}
export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProps) {
type EditingField = 'nozzle' | 'nozzle_2' | 'bed' | null;
export function TemperatureColumn({ printerId, status, nozzleCount }: TemperatureColumnProps) {
const temps = (status?.temperatures ?? {}) as Temperatures;
const isDualNozzle = nozzleCount > 1;
const isConnected = status?.connected ?? false;
const [editing, setEditing] = useState<EditingField>(null);
const [editValue, setEditValue] = useState('');
const bedMutation = useMutation({
mutationFn: (target: number) => api.setBedTemperature(printerId, target),
});
const nozzleMutation = useMutation({
mutationFn: ({ target, nozzle }: { target: number; nozzle: number }) =>
api.setNozzleTemperature(printerId, target, nozzle),
});
const lightMutation = useMutation({
mutationFn: (on: boolean) => api.setChamberLight(printerId, on),
});
const startEditing = (field: EditingField, currentValue: number) => {
if (!isConnected) return;
setEditing(field);
setEditValue(String(Math.round(currentValue)));
};
const cancelEditing = () => {
setEditing(null);
setEditValue('');
};
const submitEdit = () => {
const target = parseInt(editValue, 10);
if (isNaN(target) || target < 0) {
cancelEditing();
return;
}
if (editing === 'bed') {
bedMutation.mutate(target);
} else if (editing === 'nozzle') {
nozzleMutation.mutate({ target, nozzle: 0 });
} else if (editing === 'nozzle_2') {
nozzleMutation.mutate({ target, nozzle: 1 });
}
cancelEditing();
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
submitEdit();
} else if (e.key === 'Escape') {
cancelEditing();
}
};
const isDisabled = !isConnected;
const renderTargetTemp = (
field: EditingField,
targetValue: number
) => {
if (editing === field) {
return (
<input
type="number"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={submitEdit}
onKeyDown={handleKeyDown}
autoFocus
className="w-12 text-sm bg-bambu-dark border border-bambu-green rounded px-1 py-0.5 text-white text-center"
min={0}
max={350}
/>
);
}
return (
<button
onClick={() => startEditing(field, targetValue)}
disabled={isDisabled}
className="text-sm text-bambu-gray hover:text-bambu-green disabled:hover:text-bambu-gray disabled:cursor-not-allowed"
title={isDisabled ? 'Printer not connected' : 'Click to set target temperature'}
>
/{Math.round(targetValue)} °C
</button>
);
};
return (
<div className="flex flex-col justify-evenly min-w-[150px] pr-5 border-r border-bambu-dark-tertiary">
@@ -33,7 +124,7 @@ export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProp
</span>
)}
<span className="text-lg font-medium text-white">{Math.round(temps.nozzle ?? 0)}</span>
<span className="text-sm text-bambu-gray">/{Math.round(temps.nozzle_target ?? 0)} °C</span>
{renderTargetTemp('nozzle', temps.nozzle_target ?? 0)}
</div>
{/* Nozzle 2 (Right) - only for dual nozzle */}
@@ -46,7 +137,7 @@ export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProp
R
</span>
<span className="text-lg font-medium text-white">{Math.round(temps.nozzle_2 ?? 0)}</span>
<span className="text-sm text-bambu-gray">/{Math.round(temps.nozzle_2_target ?? 0)} °C</span>
{renderTargetTemp('nozzle_2', temps.nozzle_2_target ?? 0)}
</div>
)}
@@ -55,38 +146,42 @@ export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProp
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
<img src="/icons/heatbed.svg" alt="" className="w-5 icon-theme" />
</div>
{/* Spacer to align with L/R badge (min-w-[18px]) */}
{isDualNozzle && <span className="min-w-[18px] flex-shrink-0" />}
<span className="text-lg font-medium text-white">{Math.round(temps.bed ?? 0)}</span>
<span className="text-sm text-bambu-gray">/{Math.round(temps.bed_target ?? 0)} °C</span>
{renderTargetTemp('bed', temps.bed_target ?? 0)}
</div>
{/* Chamber */}
{/* Chamber - read only */}
<div className="flex items-center gap-1.5">
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
<img src="/icons/chamber.svg" alt="" className="w-5 icon-theme" />
</div>
{/* Spacer to align with L/R badge */}
{isDualNozzle && <span className="min-w-[18px] flex-shrink-0" />}
<span className="text-lg font-medium text-white">{Math.round(temps.chamber ?? 0)}</span>
<span className="text-sm text-bambu-gray">/{0} °C</span>
<span className="text-sm text-bambu-gray">°C</span>
</div>
{/* Air Condition - button */}
<button className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<button
disabled={isDisabled}
className="flex items-center gap-2 hover:opacity-80 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
<img src="/icons/ventilation.svg" alt="" className="w-5 icon-theme" />
</div>
<span className="text-sm text-bambu-gray">Air Condition</span>
</button>
{/* Lamp - button */}
<button className="flex items-center gap-2 hover:opacity-80 transition-opacity">
{/* Lamp - button (toggle, state not tracked in status yet) */}
<button
onClick={() => lightMutation.mutate(true)}
disabled={isDisabled || lightMutation.isPending}
className="flex items-center gap-2 hover:opacity-80 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
<img src="/icons/ventilation.svg" alt="" className="w-4 icon-theme" />
<img src="/icons/lamp.svg" alt="" className="w-4 icon-theme" />
</div>
<span className="text-sm text-bambu-gray">Lamp</span>
<div className="w-3.5 h-3.5 rounded-full bg-bambu-green" />
</button>
</div>
);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
<script type="module" crossorigin src="/assets/index-BRHFyg0M.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BzyUcjxL.css">
<script type="module" crossorigin src="/assets/index-Dg2N9nGX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Da3EoG5B.css">
</head>
<body>
<div id="root"></div>