Browse Source

SpoolBuddy touch-friendly UI overhaul for 1024x600 kiosk display

Enlarge all interactive elements across 9 SpoolBuddy components to meet
44px minimum tap targets on the RPi touchscreen. Increase nav icons
(20→24px), labels (10→12px), bar heights, section headers, printer
buttons, spool visualizations, fill bars, and status indicators.
Compact the dashboard stats bar and remove the printers card. Add
fullScreen prop to ConfigureAmsSlotModal with two-column layout
(filament list left, K-profile + color right) to eliminate scrolling.
maziggy 2 months ago
parent
commit
0200c5d92f

+ 3 - 0
CHANGELOG.md

@@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file.
 
 ## [0.2.2b1] - Unrelased
 
+### Improved
+- **SpoolBuddy Touch-Friendly UI** — Enlarged all interactive elements across the SpoolBuddy kiosk UI for comfortable finger use on the 1024×600 RPi touchscreen. Bottom nav icons and labels increased (20→24px icons, 10→12px labels, 48→56px bar height). Top bar printer selector and clock enlarged. Dashboard stats bar compacted, printers card removed (printer selection via top bar is sufficient), section headers and device status text bumped up. AMS page single-slot cards, spool visualizations, and fill bars enlarged. AMS unit cards get larger spool previews (56→64px), bigger material/slot text, and larger humidity/temperature indicators. Inventory spool cards, settings page headers, and calibration inputs all sized up to meet 44px minimum tap targets. The AMS slot configuration modal now renders in a two-column full-screen layout on the kiosk display (filament list on left, K-profile and color picker on right) instead of the standard centered dialog, eliminating scrolling.
+
 ### New Features
 - **SpoolBuddy AMS Page: External Slots & Slot Configuration** — The SpoolBuddy AMS page (`/spoolbuddy/ams`) now displays external spool slots (single nozzle: "Ext", dual nozzle: "Ext-L"/"Ext-R") and AMS-HT units in a compact horizontal row below the regular AMS grid, fitting within the 1024×600 kiosk display without scrolling. Clicking any AMS, AMS-HT, or external slot opens the `ConfigureAmsSlotModal` to configure filament type and color — the same modal used on the main Printers page. Dual-nozzle printers show L/R nozzle badges on each AMS unit. Temperature and humidity are displayed with threshold-colored SVG icons (green/gold/red) matching the Bambu Lab style on the main printer cards, using the configured AMS humidity and temperature thresholds from settings.
 - **SpoolBuddy Dashboard Redesign** — Redesigned the SpoolBuddy dashboard with a two-column layout: left column shows device connection status (scale and NFC with state-colored icons — green when device is online, gray when offline) and a compact printers list with live status indicators; right column shows the current spool card. Cards use a dashed border style for a cleaner look. The large weight display card was removed in favor of the inline scale reading in the device card.

+ 289 - 25
frontend/src/components/ConfigureAmsSlotModal.tsx

@@ -77,6 +77,7 @@ interface ConfigureAmsSlotModalProps {
   nozzleDiameter?: string;
   printerModel?: string;
   onSuccess?: () => void;
+  fullScreen?: boolean;
 }
 
 // Known filament material types
@@ -231,6 +232,7 @@ export function ConfigureAmsSlotModal({
   nozzleDiameter = '0.4',
   printerModel,
   onSuccess,
+  fullScreen,
 }: ConfigureAmsSlotModalProps) {
   const { t } = useTranslation();
   const [selectedPresetId, setSelectedPresetId] = useState<string>('');
@@ -773,20 +775,43 @@ export function ConfigureAmsSlotModal({
   const displayColor = colorHex || slotInfo.trayColor?.slice(0, 6) || 'FFFFFF';
 
   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center">
+    <div className={`fixed inset-0 z-50 flex ${fullScreen ? '' : 'items-center justify-center'}`}>
       {/* Backdrop */}
-      <div
-        className="absolute inset-0 bg-black/60 backdrop-blur-sm"
-        onClick={onClose}
-      />
+      {!fullScreen && (
+        <div
+          className="absolute inset-0 bg-black/60 backdrop-blur-sm"
+          onClick={onClose}
+        />
+      )}
 
       {/* Modal */}
-      <div className="relative w-full max-w-lg mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl">
+      <div className={fullScreen
+        ? 'relative w-full h-full bg-bambu-dark-secondary flex flex-col'
+        : 'relative w-full max-w-lg mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl'
+      }>
         {/* Header */}
-        <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
+        <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary shrink-0">
           <div className="flex items-center gap-2">
             <Settings2 className="w-5 h-5 text-bambu-blue" />
             <h2 className="text-lg font-semibold text-white">{t('configureAmsSlot.title')}</h2>
+            {/* Inline slot info in fullScreen mode */}
+            {fullScreen && (
+              <div className="flex items-center gap-2 ml-4 text-sm text-bambu-gray">
+                <span className="text-white/30">|</span>
+                {slotInfo.trayColor && (
+                  <span
+                    className="w-4 h-4 rounded-full border border-white/20"
+                    style={{ backgroundColor: `#${slotInfo.trayColor.slice(0, 6)}` }}
+                  />
+                )}
+                <span className="text-white/70">
+                  {t('configureAmsSlot.slotLabel', { ams: getAmsLabel(slotInfo.amsId, slotInfo.trayCount), slot: slotInfo.trayId + 1 })}
+                </span>
+                {slotInfo.traySubBrands && (
+                  <span>({slotInfo.traySubBrands})</span>
+                )}
+              </div>
+            )}
           </div>
           <button
             onClick={onClose}
@@ -797,7 +822,7 @@ export function ConfigureAmsSlotModal({
         </div>
 
         {/* Content */}
-        <div className="p-4 space-y-4 max-h-[60vh] overflow-y-auto">
+        <div className={`p-4 overflow-y-auto ${fullScreen ? 'flex-1 min-h-0' : 'space-y-4 max-h-[60vh]'}`}>
           {/* Success overlay */}
           {showSuccess && (
             <div className="absolute inset-0 bg-bambu-dark-secondary/95 z-10 flex items-center justify-center rounded-xl">
@@ -810,28 +835,267 @@ export function ConfigureAmsSlotModal({
           )}
 
           {/* Slot info */}
-          <div className="p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
-            <p className="text-xs text-bambu-gray mb-1">{t('configureAmsSlot.configuringSlot')}</p>
-            <div className="flex items-center gap-2">
-              {slotInfo.trayColor && (
-                <span
-                  className="w-4 h-4 rounded-full border border-white/20"
-                  style={{ backgroundColor: `#${slotInfo.trayColor.slice(0, 6)}` }}
-                />
-              )}
-              <span className="text-white font-medium">
-                {t('configureAmsSlot.slotLabel', { ams: getAmsLabel(slotInfo.amsId, slotInfo.trayCount), slot: slotInfo.trayId + 1 })}
-              </span>
-              {slotInfo.traySubBrands && (
-                <span className="text-bambu-gray">({slotInfo.traySubBrands})</span>
-              )}
+          {!fullScreen && (
+            <div className="p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
+              <p className="text-xs text-bambu-gray mb-1">{t('configureAmsSlot.configuringSlot')}</p>
+              <div className="flex items-center gap-2">
+                {slotInfo.trayColor && (
+                  <span
+                    className="w-4 h-4 rounded-full border border-white/20"
+                    style={{ backgroundColor: `#${slotInfo.trayColor.slice(0, 6)}` }}
+                  />
+                )}
+                <span className="text-white font-medium">
+                  {t('configureAmsSlot.slotLabel', { ams: getAmsLabel(slotInfo.amsId, slotInfo.trayCount), slot: slotInfo.trayId + 1 })}
+                </span>
+                {slotInfo.traySubBrands && (
+                  <span className="text-bambu-gray">({slotInfo.traySubBrands})</span>
+                )}
+              </div>
             </div>
-          </div>
+          )}
 
           {isLoading ? (
             <div className="flex justify-center py-8">
               <Loader2 className="w-6 h-6 text-bambu-green animate-spin" />
             </div>
+          ) : fullScreen ? (
+            /* Two-column layout for kiosk display */
+            <div className="flex gap-4 h-full">
+              {/* Left column: Filament preset list (takes full height) */}
+              <div className="w-1/2 flex flex-col min-h-0">
+                <label className="block text-sm text-bambu-gray mb-2">
+                  {t('configureAmsSlot.filamentProfile')} <span className="text-red-400">*</span>
+                </label>
+                <input
+                  type="text"
+                  placeholder={t('configureAmsSlot.searchPresets')}
+                  value={searchQuery}
+                  onChange={(e) => setSearchQuery(e.target.value)}
+                  className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none mb-2 shrink-0"
+                />
+                <div className="flex-1 min-h-0 overflow-y-auto space-y-1">
+                  {filteredPresets.length === 0 ? (
+                    <p className="text-center py-4 text-bambu-gray">
+                      {(cloudSettings?.filament?.length === 0 && !localPresets?.filament?.length)
+                        ? t('configureAmsSlot.noPresetsAvailable')
+                        : t('configureAmsSlot.noMatchingPresets')}
+                    </p>
+                  ) : (
+                    filteredPresets.map((preset) => (
+                      <button
+                        key={preset.id}
+                        ref={selectedPresetId === preset.id ? (el) => {
+                          el?.scrollIntoView({ block: 'nearest' });
+                        } : undefined}
+                        onClick={() => setSelectedPresetId(preset.id)}
+                        className={`w-full p-2 rounded-lg border text-left transition-colors ${
+                          selectedPresetId === preset.id
+                            ? 'bg-bambu-green/20 border-bambu-green'
+                            : 'bg-bambu-dark border-bambu-dark-tertiary hover:border-bambu-gray'
+                        }`}
+                      >
+                        <div className="flex items-center justify-between">
+                          <span className="text-white text-sm truncate">{preset.name}</span>
+                          <div className="flex items-center gap-1 flex-shrink-0">
+                            {preset.source === 'local' && (
+                              <span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400">
+                                {t('profiles.localProfiles.badge')}
+                              </span>
+                            )}
+                            {preset.source === 'builtin' && (
+                              <span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">
+                                {t('configureAmsSlot.builtin')}
+                              </span>
+                            )}
+                            {preset.isUser && (
+                              <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
+                                {t('configureAmsSlot.custom')}
+                              </span>
+                            )}
+                          </div>
+                        </div>
+                      </button>
+                    ))
+                  )}
+                </div>
+              </div>
+
+              {/* Right column: K Profile + Color */}
+              <div className="w-1/2 flex flex-col gap-4 min-h-0 overflow-y-auto">
+                {/* K Profile Select */}
+                <div>
+                  <label className="block text-sm text-bambu-gray mb-2">
+                    {t('configureAmsSlot.kProfileLabel')}
+                    {selectedMaterial && (
+                      <span className="ml-2 text-xs text-bambu-blue">
+                        {t('configureAmsSlot.filteringFor', { material: selectedMaterial })}
+                      </span>
+                    )}
+                  </label>
+                  {matchingKProfiles.length > 0 ? (
+                    <div className="relative">
+                      <select
+                        value={selectedKProfile?.name || ''}
+                        onChange={(e) => {
+                          const profile = matchingKProfiles.find(p => p.name === e.target.value);
+                          setSelectedKProfile(profile || null);
+                        }}
+                        className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none pr-10"
+                      >
+                        <option value="">{t('configureAmsSlot.noKProfile')}</option>
+                        {matchingKProfiles.map((profile) => (
+                          <option key={`${profile.name}-${profile.extruder_id}`} value={profile.name}>
+                            {profile.name} (K={profile.k_value})
+                          </option>
+                        ))}
+                      </select>
+                      <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
+                    </div>
+                  ) : selectedPresetId ? (
+                    <p className="text-sm text-bambu-gray italic py-2">
+                      {t('configureAmsSlot.noMatchingKProfiles')}
+                    </p>
+                  ) : (
+                    <span className="inline-block text-xs px-2 py-1 rounded bg-amber-500/20 text-amber-400 border border-amber-500/30">
+                      {t('configureAmsSlot.selectFilamentFirst')}
+                    </span>
+                  )}
+                  {selectedKProfile && (
+                    <p className="text-xs text-bambu-green mt-1">
+                      {t('configureAmsSlot.kFromCalibration', { value: selectedKProfile.k_value })}
+                    </p>
+                  )}
+                </div>
+
+                {/* Custom color */}
+                <div>
+                  <label className="block text-sm text-bambu-gray mb-2">
+                    {t('configureAmsSlot.customColorLabel')}
+                  </label>
+                  {catalogColors.length > 0 && (
+                    <div className="mb-3">
+                      <p className="text-xs text-bambu-gray mb-1.5">
+                        {t('configureAmsSlot.presetColors', { name: selectedPresetInfo?.fullName })}
+                      </p>
+                      <div className="flex flex-wrap gap-1.5">
+                        {catalogColors.map((entry) => (
+                          <button
+                            key={entry.id}
+                            onClick={() => {
+                              const hex = entry.hex_color.replace('#', '').toUpperCase();
+                              setColorHex(hex);
+                              setColorInput(entry.color_name);
+                            }}
+                            className={`h-7 px-2 rounded-md border-2 transition-all flex items-center gap-1.5 ${
+                              colorHex === entry.hex_color.replace('#', '').toUpperCase()
+                                ? 'border-bambu-green scale-105'
+                                : 'border-white/20 hover:border-white/40'
+                            }`}
+                            title={entry.color_name}
+                          >
+                            <span
+                              className="w-4 h-4 rounded-full border border-white/30 flex-shrink-0"
+                              style={{ backgroundColor: entry.hex_color }}
+                            />
+                            <span className="text-xs text-white/80 whitespace-nowrap">{entry.color_name}</span>
+                          </button>
+                        ))}
+                      </div>
+                    </div>
+                  )}
+                  <div className="flex flex-wrap gap-1.5 mb-2">
+                    {QUICK_COLORS_BASIC.map((color) => (
+                      <button
+                        key={color.hex}
+                        onClick={() => {
+                          setColorHex(color.hex);
+                          setColorInput(color.name);
+                        }}
+                        className={`w-7 h-7 rounded-md border-2 transition-all ${
+                          colorHex === color.hex
+                            ? 'border-bambu-green scale-110'
+                            : 'border-white/20 hover:border-white/40'
+                        }`}
+                        style={{ backgroundColor: `#${color.hex}` }}
+                        title={color.name}
+                      />
+                    ))}
+                    <button
+                      onClick={() => setShowExtendedColors(!showExtendedColors)}
+                      className="w-7 h-7 rounded-md border-2 border-white/20 hover:border-white/40 flex items-center justify-center text-white/60 hover:text-white/80 transition-all text-xs"
+                      title={showExtendedColors ? t('configureAmsSlot.showLessColors') : t('configureAmsSlot.showMoreColors')}
+                    >
+                      {showExtendedColors ? '−' : '+'}
+                    </button>
+                  </div>
+                  {showExtendedColors && (
+                    <div className="flex flex-wrap gap-1.5 mb-2">
+                      {QUICK_COLORS_EXTENDED.map((color) => (
+                        <button
+                          key={color.hex}
+                          onClick={() => {
+                            setColorHex(color.hex);
+                            setColorInput(color.name);
+                          }}
+                          className={`w-7 h-7 rounded-md border-2 transition-all ${
+                            colorHex === color.hex
+                              ? 'border-bambu-green scale-110'
+                              : 'border-white/20 hover:border-white/40'
+                          }`}
+                          style={{ backgroundColor: `#${color.hex}` }}
+                          title={color.name}
+                        />
+                      ))}
+                    </div>
+                  )}
+                  <div className="flex gap-2 items-center">
+                    <div
+                      className="w-10 h-10 rounded-lg border-2 border-white/20 flex-shrink-0"
+                      style={{ backgroundColor: `#${displayColor}` }}
+                    />
+                    <input
+                      type="text"
+                      placeholder={t('configureAmsSlot.colorPlaceholder')}
+                      value={colorInput}
+                      onChange={(e) => {
+                        const input = e.target.value;
+                        setColorInput(input);
+                        const nameHex = colorNameToHex(input);
+                        if (nameHex) {
+                          setColorHex(nameHex);
+                        } else {
+                          const cleaned = input.replace(/[^0-9A-Fa-f]/g, '').toUpperCase();
+                          if (cleaned.length === 6) {
+                            setColorHex(cleaned);
+                          } else if (cleaned.length === 3) {
+                            setColorHex(cleaned.split('').map(c => c + c).join(''));
+                          }
+                        }
+                      }}
+                      className="flex-1 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none text-sm"
+                    />
+                    {colorHex && (
+                      <button
+                        onClick={() => {
+                          setColorHex('');
+                          setColorInput('');
+                        }}
+                        className="px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded"
+                        title={t('configureAmsSlot.clearCustomColor')}
+                      >
+                        {t('configureAmsSlot.clear')}
+                      </button>
+                    )}
+                  </div>
+                  {colorHex && (
+                    <p className="text-xs text-bambu-gray mt-1.5">
+                      {t('configureAmsSlot.hexLabel', { hex: colorHex })}
+                    </p>
+                  )}
+                </div>
+              </div>
+            </div>
           ) : (
             <>
               {/* Filament Profile Select */}
@@ -1079,7 +1343,7 @@ export function ConfigureAmsSlotModal({
         </div>
 
         {/* Footer */}
-        <div className="flex justify-between p-4 border-t border-bambu-dark-tertiary">
+        <div className="flex justify-between p-4 border-t border-bambu-dark-tertiary shrink-0">
           {/* Reset button on the left */}
           <Button
             variant="secondary"

+ 11 - 11
frontend/src/components/spoolbuddy/AmsUnitCard.tsx

@@ -91,8 +91,8 @@ function HumidityIndicator({ humidity, goodThreshold = 40, fairThreshold = 60 }:
 
   return (
     <div className="flex items-center gap-0.5">
-      <DropComponent className="w-2.5 h-3" />
-      <span className="font-medium tabular-nums text-[10px]" style={{ color: textColor }}>{humidity}%</span>
+      <DropComponent className="w-3 h-3.5" />
+      <span className="font-medium tabular-nums text-xs" style={{ color: textColor }}>{humidity}%</span>
     </div>
   );
 }
@@ -114,8 +114,8 @@ function TemperatureIndicator({ temp, goodThreshold = 28, fairThreshold = 35 }:
 
   return (
     <div className="flex items-center gap-0.5">
-      <ThermoComponent className="w-2.5 h-3" />
-      <span className="font-medium tabular-nums text-[10px]" style={{ color: textColor }}>{temp}°C</span>
+      <ThermoComponent className="w-3 h-3.5" />
+      <span className="font-medium tabular-nums text-xs" style={{ color: textColor }}>{temp}°C</span>
     </div>
   );
 }
@@ -125,7 +125,7 @@ function TemperatureIndicator({ temp, goodThreshold = 28, fairThreshold = 35 }:
 function NozzleBadge({ side }: { side: 'L' | 'R' }) {
   return (
     <span
-      className="inline-flex items-center justify-center w-3.5 h-3.5 text-[8px] font-bold rounded"
+      className="inline-flex items-center justify-center w-4 h-4 text-[9px] font-bold rounded"
       style={{ backgroundColor: '#1a4d2e', color: '#00ae42' }}
     >
       {side}
@@ -148,11 +148,11 @@ function SpoolSlot({ tray, slotIndex, isActive, onClick }: SpoolSlotProps) {
 
   return (
     <div
-      className={`relative flex flex-col items-center p-2 rounded-lg transition-all ${isActive ? 'ring-2 ring-bambu-green' : ''} ${onClick ? 'cursor-pointer hover:bg-white/5' : ''}`}
+      className={`relative flex flex-col items-center p-2.5 rounded-lg transition-all ${isActive ? 'ring-2 ring-bambu-green' : ''} ${onClick ? 'cursor-pointer hover:bg-white/5' : ''}`}
       onClick={onClick}
     >
       {/* Spool visualization */}
-      <div className="relative w-14 h-14 mb-1">
+      <div className="relative w-16 h-16 mb-1">
         {isEmpty ? (
           <div className="w-full h-full rounded-full border-2 border-dashed border-gray-500 flex items-center justify-center">
             <div className="w-3 h-3 rounded-full bg-gray-600" />
@@ -167,12 +167,12 @@ function SpoolSlot({ tray, slotIndex, isActive, onClick }: SpoolSlotProps) {
           </svg>
         )}
         {isActive && (
-          <div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-bambu-green rounded-full" />
+          <div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2.5 h-2.5 bg-bambu-green rounded-full" />
         )}
       </div>
 
       {/* Material type */}
-      <span className="text-xs text-white/70 truncate max-w-full">
+      <span className="text-sm text-white/70 truncate max-w-full">
         {isEmpty ? 'Empty' : tray.tray_type || 'Unknown'}
       </span>
 
@@ -190,7 +190,7 @@ function SpoolSlot({ tray, slotIndex, isActive, onClick }: SpoolSlotProps) {
       )}
 
       {/* Slot number */}
-      <span className="absolute top-1 right-1 text-[10px] text-white/30">{slotIndex + 1}</span>
+      <span className="absolute top-1 right-1 text-xs text-white/30">{slotIndex + 1}</span>
     </div>
   );
 }
@@ -221,7 +221,7 @@ export function AmsUnitCard({ unit, activeSlot, onConfigureSlot, isDualNozzle, n
       {/* Header */}
       <div className="flex items-center justify-between mb-2">
         <div className="flex items-center gap-1.5">
-          <span className="text-white font-medium text-sm">{getAmsName(unit.id)}</span>
+          <span className="text-white font-medium text-base">{getAmsName(unit.id)}</span>
           {isDualNozzle && nozzleSide && (
             <NozzleBadge side={nozzleSide} />
           )}

+ 7 - 7
frontend/src/components/spoolbuddy/SpoolBuddyBottomNav.tsx

@@ -7,7 +7,7 @@ const navItems = [
     labelKey: 'spoolbuddy.nav.dashboard',
     fallback: 'Dashboard',
     icon: (
-      <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
         <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
       </svg>
     ),
@@ -17,7 +17,7 @@ const navItems = [
     labelKey: 'spoolbuddy.nav.ams',
     fallback: 'AMS',
     icon: (
-      <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
         <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
       </svg>
     ),
@@ -27,7 +27,7 @@ const navItems = [
     labelKey: 'spoolbuddy.nav.inventory',
     fallback: 'Inventory',
     icon: (
-      <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
         <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
       </svg>
     ),
@@ -37,7 +37,7 @@ const navItems = [
     labelKey: 'spoolbuddy.nav.settings',
     fallback: 'Settings',
     icon: (
-      <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
         <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
         <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
       </svg>
@@ -49,14 +49,14 @@ export function SpoolBuddyBottomNav() {
   const { t } = useTranslation();
 
   return (
-    <nav className="h-12 bg-bambu-dark-secondary border-t border-bambu-dark-tertiary flex items-stretch shrink-0">
+    <nav className="h-14 bg-bambu-dark-secondary border-t border-bambu-dark-tertiary flex items-stretch shrink-0">
       {navItems.map((item) => (
         <NavLink
           key={item.to}
           to={item.to}
           end={item.to === '/spoolbuddy'}
           className={({ isActive }) =>
-            `flex-1 flex flex-col items-center justify-center gap-0.5 transition-colors ${
+            `flex-1 flex flex-col items-center justify-center gap-1 transition-colors ${
               isActive
                 ? 'text-bambu-green bg-bambu-dark'
                 : 'text-white/50 hover:text-white/70 hover:bg-bambu-dark-tertiary'
@@ -64,7 +64,7 @@ export function SpoolBuddyBottomNav() {
           }
         >
           {item.icon}
-          <span className="text-[10px] font-medium">{t(item.labelKey, item.fallback)}</span>
+          <span className="text-xs font-medium">{t(item.labelKey, item.fallback)}</span>
         </NavLink>
       ))}
     </nav>

+ 2 - 2
frontend/src/components/spoolbuddy/SpoolBuddyStatusBar.tsx

@@ -29,9 +29,9 @@ export function SpoolBuddyStatusBar({ alert }: SpoolBuddyStatusBarProps) {
     : 'border-bambu-dark-tertiary';
 
   return (
-    <div className={`h-8 bg-bambu-dark-secondary border-t-2 ${borderColor} flex items-center px-3 gap-3 shrink-0`}>
+    <div className={`h-9 bg-bambu-dark-secondary border-t-2 ${borderColor} flex items-center px-3 gap-3 shrink-0`}>
       {/* Status LED */}
-      <div className={`w-3 h-3 rounded-full ${statusColor} animate-pulse`} />
+      <div className={`w-3.5 h-3.5 rounded-full ${statusColor} animate-pulse`} />
 
       {/* Status message */}
       <div className="flex-1 text-sm text-white/50 truncate">

+ 5 - 5
frontend/src/components/spoolbuddy/SpoolBuddyTopBar.tsx

@@ -50,7 +50,7 @@ export function SpoolBuddyTopBar({ selectedPrinterId, onPrinterChange, deviceOnl
     date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
 
   return (
-    <div className="h-11 bg-bambu-dark-secondary border-b border-bambu-dark-tertiary flex items-center px-3 gap-4 shrink-0">
+    <div className="h-12 bg-bambu-dark-secondary border-b border-bambu-dark-tertiary flex items-center px-3 gap-4 shrink-0">
       {/* Logo */}
       <div className="flex items-center shrink-0">
         <img src="/img/spoolbuddy_logo_dark_small.png" alt="SpoolBuddy" width={113} height={28} className="h-7 w-auto" />
@@ -61,7 +61,7 @@ export function SpoolBuddyTopBar({ selectedPrinterId, onPrinterChange, deviceOnl
         <select
           value={selectedPrinterId ?? ''}
           onChange={(e) => onPrinterChange(Number(e.target.value))}
-          className="bg-bambu-dark text-white text-sm px-3 py-1.5 rounded border border-bambu-dark-tertiary focus:outline-none focus:border-bambu-green min-w-[150px]"
+          className="bg-bambu-dark text-white text-base px-4 py-2 rounded border border-bambu-dark-tertiary focus:outline-none focus:border-bambu-green min-w-[180px]"
         >
           {onlinePrinters.length === 0 ? (
             <option value="">{t('spoolbuddy.status.noPrinters', 'No printers online')}</option>
@@ -96,12 +96,12 @@ export function SpoolBuddyTopBar({ selectedPrinterId, onPrinterChange, deviceOnl
 
         {/* Device LED */}
         <div className="flex items-center gap-1.5">
-          <div className={`w-2.5 h-2.5 rounded-full ${deviceOnline ? 'bg-bambu-green shadow-[0_0_6px_rgba(34,197,94,0.5)]' : 'bg-bambu-gray'}`} />
-          <span className="text-xs text-white/50">{deviceOnline ? t('spoolbuddy.status.online', 'Online') : t('spoolbuddy.status.offline', 'Offline')}</span>
+          <div className={`w-3 h-3 rounded-full ${deviceOnline ? 'bg-bambu-green shadow-[0_0_6px_rgba(34,197,94,0.5)]' : 'bg-bambu-gray'}`} />
+          <span className="text-sm text-white/50">{deviceOnline ? t('spoolbuddy.status.online', 'Online') : t('spoolbuddy.status.offline', 'Offline')}</span>
         </div>
 
         {/* Clock */}
-        <span className="text-white/50 text-sm font-mono min-w-[50px] text-right">
+        <span className="text-white/50 text-base font-mono min-w-[50px] text-right">
           {formatTime(currentTime)}
         </span>
       </div>

+ 4 - 4
frontend/src/components/spoolbuddy/SpoolInfoCard.tsx

@@ -131,7 +131,7 @@ export function SpoolInfoCard({ spool, scaleWeight, weightStable, onClose, onSyn
       </div>
 
       {/* Details grid */}
-      <div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm bg-zinc-800 rounded-lg p-3 w-full">
+      <div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm bg-zinc-800 rounded-lg p-4 w-full">
         <div className="flex justify-between">
           <span className="text-zinc-500">{t('spoolbuddy.dashboard.grossWeight', 'Gross weight')}</span>
           <span className="font-mono text-zinc-300">{grossWeight !== null ? `${grossWeight}g` : '\u2014'}</span>
@@ -150,16 +150,16 @@ export function SpoolInfoCard({ spool, scaleWeight, weightStable, onClose, onSyn
             <span className={`flex items-center gap-1 font-mono ${isMatch ? 'text-green-500' : 'text-yellow-500'}`}>
               {grossWeight}g
               {isMatch ? (
-                <Check className="w-3 h-3" />
+                <Check className="w-3.5 h-3.5" />
               ) : (
                 <>
-                  <AlertTriangle className="w-3 h-3" />
+                  <AlertTriangle className="w-3.5 h-3.5" />
                   <button
                     onClick={handleSyncWeight}
                     className="p-1 hover:bg-green-500/20 rounded transition-colors text-green-500"
                     title={t('spoolbuddy.dashboard.syncWeight', 'Sync Weight')}
                   >
-                    <RefreshCw className="w-3.5 h-3.5" />
+                    <RefreshCw className="w-4 h-4" />
                   </button>
                 </>
               )}

+ 7 - 6
frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx

@@ -239,7 +239,7 @@ export function SpoolBuddyAmsPage() {
   }, [htAms, vtTrays, isDualNozzle, trayNow, status?.active_extruder, t, getActiveSlotForAms, getNozzleSide, handleAmsSlotClick, handleExtSlotClick]);
 
   return (
-    <div className="h-full flex flex-col p-3">
+    <div className="h-full flex flex-col p-4">
       <div className="flex-1 min-h-0">
         {!selectedPrinterId ? (
           <div className="flex-1 flex items-center justify-center h-full">
@@ -287,11 +287,11 @@ export function SpoolBuddyAmsPage() {
                   return (
                     <div
                       key={key}
-                      className={`bg-bambu-dark-secondary rounded-lg px-2 py-1.5 cursor-pointer hover:bg-bambu-dark-secondary/80 transition-all flex items-center gap-2 ${isActive ? 'ring-2 ring-bambu-green' : ''}`}
+                      className={`bg-bambu-dark-secondary rounded-lg px-3 py-2 cursor-pointer hover:bg-bambu-dark-secondary/80 transition-all flex items-center gap-2 ${isActive ? 'ring-2 ring-bambu-green' : ''}`}
                       onClick={onClick}
                     >
                       {/* Spool */}
-                      <div className="relative w-8 h-8 flex-shrink-0">
+                      <div className="relative w-10 h-10 flex-shrink-0">
                         {isEmpty ? (
                           <div className="w-full h-full rounded-full border-2 border-dashed border-gray-500 flex items-center justify-center">
                             <div className="w-1.5 h-1.5 rounded-full bg-gray-600" />
@@ -312,10 +312,10 @@ export function SpoolBuddyAmsPage() {
                       {/* Info */}
                       <div className="min-w-0">
                         <div className="flex items-center gap-1">
-                          <span className="text-[10px] text-white/50 font-medium truncate">{label}</span>
+                          <span className="text-xs text-white/50 font-medium truncate">{label}</span>
                           {nozzleSide && <NozzleBadge side={nozzleSide} />}
                         </div>
-                        <div className="text-xs text-white/80 truncate">
+                        <div className="text-sm text-white/80 truncate">
                           {isEmpty ? 'Empty' : tray.tray_type || '?'}
                         </div>
                         {(temp != null || humidity != null) && (
@@ -339,7 +339,7 @@ export function SpoolBuddyAmsPage() {
                       </div>
                       {/* Fill bar */}
                       {!isEmpty && tray.remain != null && tray.remain >= 0 && (
-                        <div className="w-1 h-6 bg-bambu-dark-tertiary rounded-full overflow-hidden flex-shrink-0 flex flex-col-reverse">
+                        <div className="w-1.5 h-8 bg-bambu-dark-tertiary rounded-full overflow-hidden flex-shrink-0 flex flex-col-reverse">
                           <div
                             className="w-full rounded-full"
                             style={{
@@ -365,6 +365,7 @@ export function SpoolBuddyAmsPage() {
           printerId={selectedPrinterId}
           slotInfo={configureSlotModal}
           printerModel={mapModelCode(printer?.model ?? null) || undefined}
+          fullScreen
           onSuccess={() => {
             queryClient.invalidateQueries({ queryKey: ['slotPresets', selectedPrinterId] });
             queryClient.invalidateQueries({ queryKey: ['printerStatus', selectedPrinterId] });

+ 18 - 102
frontend/src/pages/spoolbuddy/SpoolBuddyDashboard.tsx

@@ -3,7 +3,7 @@ import { useOutletContext } from 'react-router-dom';
 import { useQuery } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import type { SpoolBuddyOutletContext } from '../../components/spoolbuddy/SpoolBuddyLayout';
-import { api, spoolbuddyApi, type InventorySpool, type PrinterStatus } from '../../api/client';
+import { api, spoolbuddyApi, type InventorySpool } from '../../api/client';
 import { SpoolIcon } from '../../components/spoolbuddy/SpoolIcon';
 import { SpoolInfoCard, UnknownTagCard } from '../../components/spoolbuddy/SpoolInfoCard';
 import { LinkSpoolModal } from '../../components/spoolbuddy/LinkSpoolModal';
@@ -51,7 +51,7 @@ function ColorCyclingSpool() {
 
       {/* Text content */}
       <div className="space-y-2">
-        <p className="text-lg font-medium text-zinc-300">
+        <p className="text-xl font-medium text-zinc-300">
           {t('spoolbuddy.dashboard.readyToScan', 'Ready to scan')}
         </p>
         <p className="text-sm text-zinc-500">
@@ -60,7 +60,7 @@ function ColorCyclingSpool() {
       </div>
 
       {/* Subtle hint */}
-      <div className="mt-6 flex items-center gap-2 text-xs text-zinc-600">
+      <div className="mt-6 flex items-center gap-2 text-sm text-zinc-600">
         <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
           <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
         </svg>
@@ -105,29 +105,8 @@ function DeviceOfflineState() {
 }
 
 // --- Main Dashboard ---
-// Helper to get printer status label
-function getPrinterStateLabel(state: string | null, connected: boolean): string {
-  if (!connected) return 'Offline';
-  if (!state || state === 'IDLE') return 'Idle';
-  if (state === 'RUNNING') return 'Printing';
-  if (state === 'PAUSE') return 'Paused';
-  if (state === 'FINISH') return 'Finished';
-  if (state === 'FAILED') return 'Failed';
-  return state;
-}
-
-function getPrinterStateColor(state: string | null, connected: boolean): string {
-  if (!connected) return 'bg-zinc-500';
-  if (!state || state === 'IDLE') return 'bg-bambu-green';
-  if (state === 'RUNNING') return 'bg-bambu-green animate-pulse';
-  if (state === 'PAUSE') return 'bg-amber-500';
-  if (state === 'FINISH') return 'bg-bambu-green';
-  if (state === 'FAILED') return 'bg-red-500';
-  return 'bg-zinc-500';
-}
-
 export function SpoolBuddyDashboard() {
-  const { sbState, selectedPrinterId, setSelectedPrinterId } = useOutletContext<SpoolBuddyOutletContext>();
+  const { sbState } = useOutletContext<SpoolBuddyOutletContext>();
   const { t } = useTranslation();
 
   // Fetch spools for stats, tag lookup, and untagged list
@@ -136,31 +115,6 @@ export function SpoolBuddyDashboard() {
     queryFn: () => api.getSpools(false),
   });
 
-  // Fetch printers list
-  const { data: printers = [] } = useQuery({
-    queryKey: ['printers'],
-    queryFn: () => api.getPrinters(),
-    staleTime: 30 * 1000,
-  });
-
-  // Fetch status for each printer
-  const printerStatuses = useQuery({
-    queryKey: ['printerStatuses', printers.map(p => p.id).join(',')],
-    queryFn: async () => {
-      const statuses: Record<number, PrinterStatus> = {};
-      await Promise.all(
-        printers.map(async (p) => {
-          try {
-            statuses[p.id] = await api.getPrinterStatus(p.id);
-          } catch { /* ignore */ }
-        })
-      );
-      return statuses;
-    },
-    enabled: printers.length > 0,
-    staleTime: 30 * 1000,
-  });
-
   // Current Spool card state - persists until user closes or new tag detected
   const [displayedTagId, setDisplayedTagId] = useState<string | null>(null);
   const [displayedWeight, setDisplayedWeight] = useState<number | null>(null);
@@ -271,35 +225,33 @@ export function SpoolBuddyDashboard() {
   const materials = new Set(spools.map((s) => s.material)).size;
   const brands = new Set(spools.filter((s) => s.brand).map((s) => s.brand)).size;
 
-  const statuses = printerStatuses.data ?? {};
-
   return (
     <div className="h-full flex flex-col p-4">
       {/* Compact stats bar */}
-      <div className="flex items-center gap-6 px-4 py-2 bg-zinc-800/50 rounded-xl border border-zinc-700/50 mb-4 shrink-0">
+      <div className="flex items-center gap-6 px-4 py-1.5 bg-zinc-800/50 rounded-xl border border-zinc-700/50 mb-3 shrink-0">
         <div className="flex items-center gap-2">
-          <span className="text-2xl font-bold text-zinc-100">{totalSpools}</span>
+          <span className="text-xl font-bold text-zinc-100">{totalSpools}</span>
           <span className="text-sm text-zinc-500">{t('spoolbuddy.inventory.spools', 'Spools')}</span>
         </div>
-        <div className="w-px h-6 bg-zinc-700" />
+        <div className="w-px h-5 bg-zinc-700" />
         <div className="flex items-center gap-2">
-          <span className="text-2xl font-bold text-zinc-100">{materials}</span>
+          <span className="text-xl font-bold text-zinc-100">{materials}</span>
           <span className="text-sm text-zinc-500">{t('spoolbuddy.spool.material', 'Materials')}</span>
         </div>
-        <div className="w-px h-6 bg-zinc-700" />
+        <div className="w-px h-5 bg-zinc-700" />
         <div className="flex items-center gap-2">
-          <span className="text-2xl font-bold text-zinc-100">{brands}</span>
+          <span className="text-xl font-bold text-zinc-100">{brands}</span>
           <span className="text-sm text-zinc-500">{t('spoolbuddy.spool.brand', 'Brands')}</span>
         </div>
       </div>
 
-      {/* Main content: Device + Printers (left) + Current Spool (right) */}
+      {/* Main content: Device (left) + Current Spool (right) */}
       <div className="flex-1 flex gap-4 min-h-0">
         {/* Left column */}
-        <div className="w-5/12 flex flex-col gap-4 min-h-0">
+        <div className="w-5/12 flex flex-col min-h-0">
           {/* Device card */}
-          <div className="border border-dashed border-zinc-700/50 rounded-xl p-4 shrink-0">
-            <h2 className="text-xs font-semibold text-zinc-400 uppercase tracking-wide mb-3">
+          <div className="border border-dashed border-zinc-700/50 rounded-xl p-4">
+            <h2 className="text-sm font-semibold text-zinc-400 uppercase tracking-wide mb-3">
               {t('spoolbuddy.dashboard.device', 'Device')}
             </h2>
 
@@ -307,7 +259,7 @@ export function SpoolBuddyDashboard() {
               {/* Connection status */}
               <div className="flex items-center gap-3">
                 <div className={`w-2.5 h-2.5 rounded-full ${sbState.deviceOnline ? 'bg-green-500 animate-pulse' : 'bg-red-500'}`} />
-                <span className="text-sm text-zinc-400">
+                <span className="text-base text-zinc-400">
                   {sbState.deviceOnline ? t('spoolbuddy.status.online', 'Online') : t('spoolbuddy.status.offline', 'Disconnected')}
                 </span>
               </div>
@@ -318,7 +270,7 @@ export function SpoolBuddyDashboard() {
                   <svg className={`w-4 h-4 ${sbState.deviceOnline ? 'text-green-500' : 'text-zinc-500'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                     <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
                   </svg>
-                  <span className="text-xs text-zinc-500">{t('spoolbuddy.spool.scaleWeight', 'Scale')}</span>
+                  <span className="text-sm text-zinc-500">{t('spoolbuddy.spool.scaleWeight', 'Scale')}</span>
                 </div>
                 <span className="text-lg font-mono font-semibold text-zinc-100">
                   {scaleDisplayValue !== null ? `${Math.abs(scaleDisplayValue) <= 20 ? 0 : Math.round(Math.max(0, scaleDisplayValue))}g` : '\u2014'}
@@ -331,7 +283,7 @@ export function SpoolBuddyDashboard() {
                   <svg className={`w-4 h-4 ${sbState.deviceOnline ? 'text-green-500' : 'text-zinc-500'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                     <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A2 2 0 013 12V7a4 4 0 014-4z" />
                   </svg>
-                  <span className="text-xs text-zinc-500">NFC</span>
+                  <span className="text-sm text-zinc-500">NFC</span>
                 </div>
                 <span className={`text-sm font-medium ${currentTagId ? 'text-green-500' : 'text-zinc-500'}`}>
                   {currentTagId ? t('spoolbuddy.dashboard.tagDetected', 'Tag detected') : t('spoolbuddy.dashboard.noTag', 'No tag')}
@@ -339,48 +291,12 @@ export function SpoolBuddyDashboard() {
               </div>
             </div>
           </div>
-
-          {/* Printers card */}
-          <div className="border border-dashed border-zinc-700/50 rounded-xl p-4 flex-1 min-h-0 flex flex-col">
-            <h2 className="text-xs font-semibold text-zinc-400 uppercase tracking-wide mb-2">
-              {t('spoolbuddy.dashboard.printers', 'Printers')}
-            </h2>
-            <div className="space-y-1.5 overflow-y-auto flex-1 min-h-0">
-              {printers.length === 0 ? (
-                <p className="text-sm text-zinc-600">{t('spoolbuddy.dashboard.noPrinters', 'No printers configured')}</p>
-              ) : (
-                printers.map((p) => {
-                  const st = statuses[p.id];
-                  const stateLabel = getPrinterStateLabel(st?.state ?? null, st?.connected ?? false);
-                  const stateColor = getPrinterStateColor(st?.state ?? null, st?.connected ?? false);
-                  const isSelected = selectedPrinterId === p.id;
-                  return (
-                    <button
-                      key={p.id}
-                      onClick={() => setSelectedPrinterId(p.id)}
-                      className={`w-full text-left py-1.5 px-3 bg-zinc-800/50 rounded-lg border-l-2 transition-colors hover:bg-zinc-800 ${
-                        isSelected ? 'border-l-bambu-green' : 'border-l-bambu-green/40'
-                      }`}
-                    >
-                      <div className="flex items-center justify-between">
-                        <span className="text-sm font-medium text-zinc-200">{p.name}</span>
-                        <div className="flex items-center gap-1.5">
-                          <div className={`w-1.5 h-1.5 rounded-full ${stateColor}`} />
-                          <span className="text-xs text-zinc-500">{stateLabel}</span>
-                        </div>
-                      </div>
-                    </button>
-                  );
-                })
-              )}
-            </div>
-          </div>
         </div>
 
         {/* Right column: Current Spool */}
         <div className="w-7/12 min-h-0">
           <div className="border border-dashed border-zinc-700/50 rounded-xl p-6 h-full flex flex-col">
-            <h2 className="text-xs font-semibold text-zinc-400 uppercase tracking-wide mb-4 shrink-0">
+            <h2 className="text-sm font-semibold text-zinc-400 uppercase tracking-wide mb-4 shrink-0">
               {t('spoolbuddy.dashboard.currentSpool', 'Current Spool')}
             </h2>
             <div className="flex-1 flex items-center justify-center min-h-0">

+ 10 - 10
frontend/src/pages/spoolbuddy/SpoolBuddyInventoryPage.tsx

@@ -25,7 +25,7 @@ function SpoolCard({ spool }: { spool: InventorySpool }) {
 
   return (
     <button
-      className="w-full bg-zinc-800 rounded-lg p-3 text-left transition-colors hover:bg-zinc-750 active:bg-zinc-700"
+      className="w-full bg-zinc-800 rounded-lg p-3.5 text-left transition-colors hover:bg-zinc-750 active:bg-zinc-700"
       onClick={() => setExpanded(!expanded)}
     >
       <div className="flex items-center gap-3">
@@ -35,7 +35,7 @@ function SpoolCard({ spool }: { spool: InventorySpool }) {
         {/* Info */}
         <div className="flex-1 min-w-0">
           <div className="flex items-center gap-2">
-            <span className="text-sm font-medium text-zinc-100 truncate">
+            <span className="text-base font-medium text-zinc-100 truncate">
               {spool.color_name || spool.material}
             </span>
             {spool.tag_uid && (
@@ -44,20 +44,20 @@ function SpoolCard({ spool }: { spool: InventorySpool }) {
               </svg>
             )}
           </div>
-          <span className="text-xs text-zinc-500 truncate block">
+          <span className="text-sm text-zinc-500 truncate block">
             {spool.brand ? `${spool.brand} \u2022 ` : ''}{spool.material}{spool.subtype ? ` ${spool.subtype}` : ''}
           </span>
         </div>
 
         {/* Weight */}
         <div className="text-right shrink-0">
-          <div className="text-sm text-zinc-300">{Math.round(remaining)}g</div>
-          <div className="text-xs text-zinc-500">{t('spoolbuddy.spool.remaining', 'Remaining')}</div>
+          <div className="text-base text-zinc-300">{Math.round(remaining)}g</div>
+          <div className="text-sm text-zinc-500">{t('spoolbuddy.spool.remaining', 'Remaining')}</div>
         </div>
       </div>
 
       {/* Fill bar */}
-      <div className="w-full h-1 bg-zinc-700 rounded-full overflow-hidden mt-2">
+      <div className="w-full h-1.5 bg-zinc-700 rounded-full overflow-hidden mt-2">
         <div
           className="h-full rounded-full transition-all"
           style={{ width: `${Math.min(100, remainingPct)}%`, backgroundColor: fillColor }}
@@ -66,7 +66,7 @@ function SpoolCard({ spool }: { spool: InventorySpool }) {
 
       {/* Expanded details */}
       {expanded && (
-        <div className="mt-3 pt-3 border-t border-zinc-700 grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
+        <div className="mt-3 pt-3 border-t border-zinc-700 grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
           <div className="flex justify-between">
             <span className="text-zinc-500">{t('spoolbuddy.spool.labelWeight', 'Label')}</span>
             <span className="text-zinc-400">{spool.label_weight}g</span>
@@ -86,7 +86,7 @@ function SpoolCard({ spool }: { spool: InventorySpool }) {
           {spool.tag_uid && (
             <div className="col-span-2 flex justify-between">
               <span className="text-zinc-500">Tag</span>
-              <span className="text-zinc-400 font-mono text-[10px]">{spool.tag_uid}</span>
+              <span className="text-zinc-400 font-mono text-xs">{spool.tag_uid}</span>
             </div>
           )}
         </div>
@@ -118,7 +118,7 @@ export function SpoolBuddyInventoryPage() {
     <div className="h-full flex flex-col p-4">
       {/* Header */}
       <div className="flex items-center justify-between mb-4">
-        <h1 className="text-lg font-semibold text-zinc-100">
+        <h1 className="text-xl font-semibold text-zinc-100">
           {t('spoolbuddy.nav.inventory', 'Inventory')}
         </h1>
         <span className="text-sm text-zinc-500">{spools.length} {t('spoolbuddy.inventory.spools', 'spools')}</span>
@@ -126,7 +126,7 @@ export function SpoolBuddyInventoryPage() {
 
       {/* Search */}
       <div className="relative mb-4">
-        <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+        <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
           <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
         </svg>
         <input

+ 4 - 4
frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx

@@ -67,7 +67,7 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: {
 
   return (
     <div className="bg-zinc-800 rounded-lg p-4">
-      <h3 className="text-sm font-semibold text-zinc-100 mb-4">
+      <h3 className="text-base font-semibold text-zinc-100 mb-4">
         {t('spoolbuddy.settings.scaleCalibration', 'Scale Calibration')}
       </h3>
 
@@ -126,7 +126,7 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: {
                 type="number"
                 value={knownWeight}
                 onChange={(e) => setKnownWeight(Number(e.target.value))}
-                className="w-24 px-2 py-1.5 bg-zinc-900 border border-zinc-600 rounded text-sm text-zinc-100 focus:outline-none focus:border-green-500"
+                className="w-24 px-2 py-2 bg-zinc-900 border border-zinc-600 rounded text-sm text-zinc-100 focus:outline-none focus:border-green-500 min-h-[44px]"
                 min={1}
               />
             </div>
@@ -158,7 +158,7 @@ function DeviceInfoCard({ device }: { device: SpoolBuddyDevice }) {
 
   return (
     <div className="bg-zinc-800 rounded-lg p-4">
-      <h3 className="text-sm font-semibold text-zinc-100 mb-4">
+      <h3 className="text-base font-semibold text-zinc-100 mb-4">
         {t('spoolbuddy.settings.deviceInfo', 'Device Info')}
       </h3>
 
@@ -223,7 +223,7 @@ export function SpoolBuddySettingsPage() {
 
   return (
     <div className="h-full flex flex-col p-4">
-      <h1 className="text-lg font-semibold text-zinc-100 mb-4">
+      <h1 className="text-xl font-semibold text-zinc-100 mb-4">
         {t('spoolbuddy.nav.settings', 'Settings')}
       </h1>
 

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CS9we2y6.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DAOWLKX-.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-OUDlnhQe.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DYx-SdRP.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-DAOWLKX-.css">
+    <script type="module" crossorigin src="/assets/index-CS9we2y6.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-OUDlnhQe.css">
   </head>
   <body>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff