Browse Source

Fix SpoolBuddy virtual keyboard layout and number input support

  Virtual keyboard now participates in flex layout instead of fixed
  overlay, eliminating dead space between content and keyboard. Bottom
  nav and status bar hide when keyboard is open. Number inputs (e.g.
  Weight field on Write Tag) now accept virtual keyboard input.
maziggy 2 months ago
parent
commit
6b93cf806e

+ 1 - 1
CHANGELOG.md

@@ -23,7 +23,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **SpoolBuddy Inventory Page** — Added a new Inventory page to the SpoolBuddy kiosk UI, accessible from the bottom navigation bar between Write and Settings. Shows a responsive catalog grid of spools with colored spool circles (matching AMS page style), material/subtype labels, color dots, fill level bars, remaining weight with percentage, and green AMS location badges (A1, B2, etc.) for assigned spools. Includes a search bar (filters by material, subtype, brand, color, notes) and touch-friendly inline filter pills ("All", "In AMS", per-material). Tapping a spool opens a full-screen detail view with spool icon, remaining bar, AMS assignment, weight breakdown, slicer filament, PA K-profiles (name and value), temperature range, cost, tag ID, and notes. Detail view updates live from query data. Assigned spools sort first. When Spoolman is enabled, the page shows the Spoolman UI instead.
 - **SpoolBuddy Auto-Navigate on Tag Scan** — When an NFC tag is detected while the SpoolBuddy UI is on a non-dashboard page (Settings, AMS, Write Tag, etc.), the frontend automatically navigates back to the main dashboard to show the scanned spool. Also wakes the screen if the display was blanked.
 - **SpoolBuddy Swipe to Switch Printers** — Swiping left/right on the SpoolBuddy touchscreen now cycles through online printers instead of triggering browser back/forward navigation. The selected printer updates in the top bar dropdown. Requires at least two online printers; single-printer setups are unaffected.
-- **SpoolBuddy Virtual Keyboard No Longer Overlays Input Fields** — The virtual keyboard now adds temporary scroll padding to the content area when it opens, ensuring the focused input field scrolls above the keyboard instead of being hidden behind it. Fixes text entry on the SpoolBuddy Settings device tab (backend URL, API token fields).
+- **SpoolBuddy Virtual Keyboard Layout Fix** — The virtual keyboard now participates in the flex layout instead of overlaying as a fixed element. When the keyboard opens, the bottom nav and status bar are hidden and the content area shrinks to fit, eliminating the dead space gap between content and keyboard on the Inventory page. Number inputs (e.g. Weight field on Write Tag) now accept virtual keyboard input.
 - **Removed Diagnostic Buttons from Write Tag Page** — Removed the "NFC Diag" and "Scale Diag" buttons from the NFC status panel on the Write Tag page. These diagnostics are accessible from the Settings page and don't belong on the tag writing flow.
 - **SpoolBuddy Assign Spool Modal No Longer Clips Display** — The shared Assign Spool modal overflowed off-screen on the small SpoolBuddy touchscreen, hiding the footer buttons. Added scoped CSS in the SpoolBuddy AMS page that caps the modal at 90vh with a scrollable spool list, without affecting the main Bambuddy frontend.
 - **SpoolBuddy System Tab** — Added a "System" tab to SpoolBuddy Settings showing live OS stats from the Raspberry Pi: CPU temperature, core count, load average, memory usage, disk usage, OS distro/kernel/architecture, Python version, and system uptime. Stats are collected by the daemon every heartbeat (10s) using stdlib-only reads from `/proc` and `/sys` — no additional dependencies required. Usage bars turn amber at 70% and red at 90%; CPU temperature is color-coded green/amber/red.

+ 12 - 35
frontend/src/components/VirtualKeyboard.tsx

@@ -3,7 +3,7 @@ import Keyboard from 'react-simple-keyboard';
 import 'react-simple-keyboard/build/css/index.css';
 import './VirtualKeyboard.css';
 
-const FOCUSABLE_TYPES = new Set(['text', 'password', 'email', 'search', 'url']);
+const FOCUSABLE_TYPES = new Set(['text', 'password', 'email', 'search', 'url', 'number']);
 
 /**
  * Set value on a controlled React input using the native setter,
@@ -17,7 +17,7 @@ function setNativeValue(input: HTMLInputElement | HTMLTextAreaElement, value: st
   input.dispatchEvent(new Event('input', { bubbles: true }));
 }
 
-export function VirtualKeyboard() {
+export function VirtualKeyboard({ onVisibilityChange }: { onVisibilityChange?: (visible: boolean) => void }) {
   const [visible, setVisible] = useState(false);
   const [closing, setClosing] = useState(false);
   const closingRef = useRef(false);
@@ -26,29 +26,10 @@ export function VirtualKeyboard() {
   const keyboardRef = useRef<ReturnType<typeof Keyboard> | null>(null);
   const containerRef = useRef<HTMLDivElement>(null);
 
-  // Add bottom padding to the scrollable ancestor so inputs can scroll above the keyboard
-  const paddedParentRef = useRef<HTMLElement | null>(null);
-
-  const addScrollPadding = useCallback((target: HTMLElement) => {
-    // Find nearest scrollable ancestor
-    let el: HTMLElement | null = target.parentElement;
-    while (el) {
-      const style = getComputedStyle(el);
-      if (style.overflowY === 'auto' || style.overflowY === 'scroll') break;
-      el = el.parentElement;
-    }
-    if (!el) return;
-    paddedParentRef.current = el;
-    // Keyboard is ~260px tall; add generous padding
-    el.style.paddingBottom = '280px';
-  }, []);
-
-  const removeScrollPadding = useCallback(() => {
-    if (paddedParentRef.current) {
-      paddedParentRef.current.style.paddingBottom = '';
-      paddedParentRef.current = null;
-    }
-  }, []);
+  // Notify parent when keyboard visibility changes
+  useEffect(() => {
+    onVisibilityChange?.(visible);
+  }, [visible, onVisibilityChange]);
 
   const handleFocusIn = useCallback((e: FocusEvent) => {
     if (closingRef.current) return;
@@ -71,14 +52,12 @@ export function VirtualKeyboard() {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     (keyboardRef.current as any)?.setInput?.(activeInput.current.value);
 
-    // Add scroll padding then scroll the input's parent card into view above the keyboard
-    addScrollPadding(target);
+    // Scroll focused input into view after the keyboard renders and layout reflows
     setTimeout(() => {
-      // Scroll the closest card/section so the entire group of fields is visible
       const card = target.closest('.bg-zinc-800, .rounded-lg, [data-vkb-group]') as HTMLElement | null;
-      (card ?? target).scrollIntoView({ behavior: 'smooth', block: 'start' });
+      (card ?? target).scrollIntoView({ behavior: 'smooth', block: 'nearest' });
     }, 100);
-  }, [addScrollPadding]);
+  }, []);
 
   const handleFocusOut = useCallback(() => {
     // Delay to allow click on keyboard buttons to register
@@ -91,11 +70,10 @@ export function VirtualKeyboard() {
       ) {
         return;
       }
-      removeScrollPadding();
       setVisible(false);
       activeInput.current = null;
     }, 150);
-  }, [removeScrollPadding]);
+  }, []);
 
   useEffect(() => {
     document.addEventListener('focusin', handleFocusIn);
@@ -111,7 +89,6 @@ export function VirtualKeyboard() {
   const dismiss = useCallback(() => {
     closingRef.current = true;
     setClosing(true);
-    removeScrollPadding();
     activeInput.current?.blur();
     activeInput.current = null;
     setTimeout(() => {
@@ -119,7 +96,7 @@ export function VirtualKeyboard() {
       setClosing(false);
       closingRef.current = false;
     }, 400);
-  }, [removeScrollPadding]);
+  }, []);
 
   const onKeyPress = useCallback((button: string) => {
     const input = activeInput.current;
@@ -171,7 +148,7 @@ export function VirtualKeyboard() {
       {!closing && (
       <div
         ref={containerRef}
-        className="fixed bottom-0 left-0 right-0 z-[9999]"
+        className="relative z-[9999] shrink-0"
         onMouseDown={(e) => e.preventDefault()}
         onTouchStart={(e) => {
           // Prevent focus loss but allow button interaction

+ 6 - 3
frontend/src/components/spoolbuddy/SpoolBuddyLayout.tsx

@@ -180,6 +180,9 @@ export function SpoolBuddyLayout() {
     return () => el.removeEventListener('touchmove', onTouchMove);
   }, []);
 
+  // Track virtual keyboard visibility to hide bottom bars
+  const [keyboardVisible, setKeyboardVisible] = useState(false);
+
   // CSS brightness filter (software dimming)
   const brightnessStyle = displayBrightness < 100
     ? { filter: `brightness(${displayBrightness / 100})` } as const
@@ -209,9 +212,9 @@ export function SpoolBuddyLayout() {
           }} />
         </main>
 
-        <SpoolBuddyStatusBar alert={alert} />
-        <SpoolBuddyBottomNav />
-        <VirtualKeyboard />
+        {!keyboardVisible && <SpoolBuddyStatusBar alert={alert} />}
+        {!keyboardVisible && <SpoolBuddyBottomNav />}
+        <VirtualKeyboard onVisibilityChange={setKeyboardVisible} />
       </div>
 
       {/* Screen blank overlay — touch to wake */}

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


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BXqghasR.js"></script>
+    <script type="module" crossorigin src="/assets/index-Bqi5VOJF.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DWwO6mIe.css">
   </head>
   <body>

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