diff --git a/backend/package.json b/backend/package.json index b3b183b..b59fd71 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.37", + "version": "0.12.38", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index b88a254..6183c6a 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.37}" +VERSION="${1:-0.12.38}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index 5c9eb0d..b5760be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.37", + "version": "0.12.38", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/MobileEventForm.jsx b/frontend/src/components/MobileEventForm.jsx index 8b2a089..ac028ab 100644 --- a/frontend/src/components/MobileEventForm.jsx +++ b/frontend/src/components/MobileEventForm.jsx @@ -68,15 +68,48 @@ function fmt12(val) { return `${h}:${String(mm).padStart(2,'0')} ${ampm}`; } -// Mobile TimeInput — same behaviour as desktop but styled for mobile inline use +// Mobile TimeInput — free-text time entry with smart-positioned scrollable dropdown function TimeInputMobile({ value, onChange }) { const [open, setOpen] = useState(false); const [inputVal, setInputVal] = useState(fmt12(value)); + const [keyboardOffset, setKeyboardOffset] = useState(0); + const [dropdownPos, setDropdownPos] = useState({ top: 0, bottom: 'auto', left: 0 }); const wrapRef = useRef(null); const listRef = useRef(null); useEffect(() => { setInputVal(fmt12(value)); }, [value]); + // Detect keyboard height via Visual Viewport API + useEffect(() => { + const handleViewportChange = () => { + if (window.visualViewport) { + const offset = window.innerHeight - window.visualViewport.height; + setKeyboardOffset(offset > 0 ? offset : 0); + } + }; + if (open) { + handleViewportChange(); + window.visualViewport?.addEventListener('resize', handleViewportChange); + return () => window.visualViewport?.removeEventListener('resize', handleViewportChange); + } else { + setKeyboardOffset(0); + } + }, [open]); + + // Calculate dropdown pixel position using getBoundingClientRect (required for position:fixed) + useEffect(() => { + if (open && wrapRef.current) { + const rect = wrapRef.current.getBoundingClientRect(); + const dropdownHeight = 5 * 40; + const spaceBelow = window.innerHeight - rect.bottom - keyboardOffset; + if (spaceBelow >= dropdownHeight) { + setDropdownPos({ top: rect.bottom, bottom: 'auto', left: rect.left }); + } else { + setDropdownPos({ top: 'auto', bottom: window.innerHeight - rect.top + keyboardOffset, left: rect.left }); + } + } + }, [open, keyboardOffset]); + useEffect(() => { if (!open || !listRef.current) return; const idx = TIME_SLOTS.findIndex(s => s.value === value); @@ -100,22 +133,30 @@ function TimeInputMobile({ value, onChange }) { return (