Update PedigreeTree and RabbitDetail components for improved ear number handling and display
This commit is contained in:
parent
4544aa8060
commit
99a1c65c9f
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
|
||||
|
||||
# MyKindlebox — Implementation Plan
|
||||
|
||||
**Version:** 1.1
|
||||
|
|
|
|||
|
|
@ -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
|
|||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute left-0 top-full z-10 mt-1 w-64 rounded-md border border-border bg-surface-raised p-2 shadow-sm">
|
||||
<div
|
||||
className={`absolute left-0 z-10 w-64 rounded-md border border-border bg-surface-raised p-2 shadow-sm ${
|
||||
dropUp ? 'bottom-full mb-1' : 'top-full mt-1'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-2 flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -223,9 +247,9 @@ function AncestorSlot({ path, slot, label, state, setSlotState, cascadeCache, fe
|
|||
<input
|
||||
type="text"
|
||||
value={manualForm.ear_number}
|
||||
onChange={(e) => setManualField('ear_number', e.target.value)}
|
||||
onChange={(e) => setManualField('ear_number', e.target.value.toUpperCase())}
|
||||
placeholder="Ear Number *"
|
||||
className="w-full rounded border border-border px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
className="w-full rounded border border-border px-2 py-1 text-sm uppercase focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
/>
|
||||
<div>
|
||||
<label className="block px-0.5 font-body text-xs text-text-secondary">Weight (lbs)</label>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ function AncestorNodeView({ path, label, ancestors }) {
|
|||
{node.name}
|
||||
{node.farm ? ` (${node.farm})` : ''}
|
||||
</span>
|
||||
{node.ear_number ? (
|
||||
<span className="w-full truncate font-body text-xs text-text-secondary">Ear #{node.ear_number}</span>
|
||||
) : null}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -48,19 +51,26 @@ function Generation({ entries, ancestors }) {
|
|||
)
|
||||
}
|
||||
|
||||
function PedigreeTreeReadOnly({ subjectName, ancestors }) {
|
||||
// `generations` controls how many ancestor columns to render. The detail page
|
||||
// uses 2 (parents + grandparents) to keep the inline tree compact; the full
|
||||
// 3-generation tree lives on the dedicated print page.
|
||||
function PedigreeTreeReadOnly({ subjectName, ancestors, generations = 3 }) {
|
||||
// Each generation column is ~176px (incl. gap); narrower min-width for fewer
|
||||
// generations lets the tree fit its column without horizontal scrolling.
|
||||
const minWidth = generations >= 3 ? 720 : 544
|
||||
|
||||
return (
|
||||
// The scroll container fills (and shrinks with) its column; the 720px-wide
|
||||
// tree scrolls horizontally inside it rather than forcing the page wider and
|
||||
// pushing the timeline column off-screen.
|
||||
// The scroll container fills (and shrinks with) its column; the tree scrolls
|
||||
// horizontally inside it rather than forcing the page wider and pushing the
|
||||
// timeline column off-screen.
|
||||
<div className="w-full overflow-x-auto p-4">
|
||||
<div className="flex min-w-[720px] items-center gap-4">
|
||||
<div className="flex items-center gap-4" style={{ minWidth }}>
|
||||
<div className="flex h-16 w-28 shrink-0 items-center justify-center rounded-md border-2 border-accent bg-surface px-3 text-center">
|
||||
<span className="truncate font-body text-sm font-medium text-text-primary">{subjectName}</span>
|
||||
</div>
|
||||
<Generation entries={GEN1} ancestors={ancestors} />
|
||||
<Generation entries={GEN2} ancestors={ancestors} />
|
||||
<Generation entries={GEN3} ancestors={ancestors} />
|
||||
{generations >= 2 && <Generation entries={GEN2} ancestors={ancestors} />}
|
||||
{generations >= 3 && <Generation entries={GEN3} ancestors={ancestors} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -166,8 +166,8 @@ function RabbitDetailsForm({ value, onChange, onPhotoChange, weightUnit = 'decim
|
|||
<input
|
||||
type="text"
|
||||
value={value.ear_number ?? ''}
|
||||
onChange={(e) => setField('ear_number', e.target.value)}
|
||||
className="mt-1 w-full rounded-md border border-border px-3 py-2 font-body text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
onChange={(e) => setField('ear_number', e.target.value.toUpperCase())}
|
||||
className="mt-1 w-full rounded-md border border-border px-3 py-2 font-body text-sm uppercase focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ function RabbitDetail() {
|
|||
<div className="min-w-0 flex-1 space-y-6">
|
||||
<Card className="overflow-hidden">
|
||||
{ancestors ? (
|
||||
<PedigreeTreeReadOnly subjectName={rabbit.name} ancestors={ancestors} />
|
||||
<PedigreeTreeReadOnly subjectName={rabbit.name} ancestors={ancestors} generations={2} />
|
||||
) : (
|
||||
<p className="p-6 font-body text-sm text-text-secondary">Loading pedigree…</p>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue