From 99a1c65c9ffcf6924799910f95fc855ece81a1ea Mon Sep 17 00:00:00 2001 From: fireshaper Date: Thu, 25 Jun 2026 23:34:48 -0400 Subject: [PATCH] Update PedigreeTree and RabbitDetail components for improved ear number handling and display --- memory-bank/implementation-plan.md | 3 -- .../client/src/components/PedigreeTree.jsx | 32 ++++++++++++++++--- .../src/components/PedigreeTreeReadOnly.jsx | 24 ++++++++++---- .../src/components/RabbitDetailsForm.jsx | 4 +-- mykindlebox/client/src/pages/RabbitDetail.jsx | 2 +- 5 files changed, 48 insertions(+), 17 deletions(-) diff --git a/memory-bank/implementation-plan.md b/memory-bank/implementation-plan.md index b704720..53a2f1f 100644 --- a/memory-bank/implementation-plan.md +++ b/memory-bank/implementation-plan.md @@ -1,6 +1,3 @@ - - - # MyKindlebox — Implementation Plan **Version:** 1.1 diff --git a/mykindlebox/client/src/components/PedigreeTree.jsx b/mykindlebox/client/src/components/PedigreeTree.jsx index 06cff5e..a0fe974 100644 --- a/mykindlebox/client/src/components/PedigreeTree.jsx +++ b/mykindlebox/client/src/components/PedigreeTree.jsx @@ -14,7 +14,12 @@ import { formatManualAncestorDisplay, } from '../lib/pedigree' -const EMPTY_MANUAL_FORM = { name: '', farm: '', ear_number: '', weight_lbs: '0', dob: '', dobUnknown: true } +const EMPTY_MANUAL_FORM = { name: '', farm: '', ear_number: '', weight_lbs: '0', dob: '', dobUnknown: false } + +// Upper bound on the popup's height (its tallest state is the manual-entry +// form). Used to decide drop direction synchronously on open, before the popup +// mounts, so the very first click is positioned correctly. +const POPUP_MAX_HEIGHT = 360 // `dob` on the manual-ancestor form is a real date input value; `dobUnknown` // is the separate "Unknown" checkbox. They collapse into one stored string @@ -40,6 +45,20 @@ function AncestorSlot({ path, slot, label, state, setSlotState, cascadeCache, fe const containerRef = useRef(null) const inputRef = useRef(null) const triggerRef = useRef(null) + // Open the popup above the trigger when there isn't room below it (e.g. nodes + // near the bottom of the window) so the tall manual form doesn't get cut off. + const [dropUp, setDropUp] = useState(false) + + // Decide drop direction from the trigger's position in the viewport. Called + // synchronously on open (the trigger already exists in the DOM) so the first + // click lands in the right place rather than flipping on the next open. + function updateDropUp() { + if (!triggerRef.current) return + const rect = triggerRef.current.getBoundingClientRect() + const spaceBelow = window.innerHeight - rect.bottom + const spaceAbove = rect.top + setDropUp(spaceBelow < POPUP_MAX_HEIGHT + 8 && spaceAbove > spaceBelow) + } function close() { setOpen(false) @@ -125,6 +144,7 @@ function AncestorSlot({ path, slot, label, state, setSlotState, cascadeCache, fe ref={triggerRef} type="button" onClick={() => { + if (!open) updateDropUp() setOpen((v) => !v) if (node.mode === 'manual') { setMode('manual') @@ -145,7 +165,11 @@ function AncestorSlot({ path, slot, label, state, setSlotState, cascadeCache, fe {open && ( -
+