Person page: one-click sex setter (no edit mode)

Setting a person's sex meant clicking Edit, opening a dropdown, and saving.
Replace the read-only ♂/♀ symbol next to the name with an always-visible
two-button segmented control that PATCHes immediately on click (gender-only;
backend PATCH is exclude_unset so the name/other fields are untouched).
Clicking the active sex clears it. The full edit form still offers gender for
completeness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
2026-06-08 21:42:59 -04:00
parent 7255920135
commit 93c22b4bcf
@@ -535,6 +535,16 @@ export default function PersonDetailPage() {
setEditingPerson(true);
}
// Quick one-click sex setter — no need to open the full edit form. PATCH is
// exclude_unset on the backend, so sending only `gender` leaves the rest.
async function setGender(value: "male" | "female" | null) {
await api.PATCH("/api/v1/trees/{tree_id}/persons/{person_id}", {
params: { path: { tree_id: treeId, person_id: personId } },
body: { gender: value },
});
load();
}
async function savePerson() {
const { error } = await api.PATCH("/api/v1/trees/{tree_id}/persons/{person_id}", {
params: { path: { tree_id: treeId, person_id: personId } },
@@ -711,16 +721,35 @@ export default function PersonDetailPage() {
<h1 className="flex flex-wrap items-center gap-3 text-3xl font-semibold">
<span className="inline-flex items-center gap-2">
{person.primary_name ?? "Unnamed person"}
{person.gender === "male" && (
<span title="Male" aria-label="Male" style={{ color: "rgb(120, 159, 172)" }}>
</span>
)}
{person.gender === "female" && (
<span title="Female" aria-label="Female" style={{ color: "rgb(196, 138, 146)" }}>
</span>
)}
</span>
{/* One-click sex setter — no edit mode needed. Active = current; click it again to clear. */}
<span className="inline-flex items-center overflow-hidden rounded-md border border-[var(--border)] text-base font-normal">
<button
type="button"
onClick={() => setGender(person.gender === "male" ? null : "male")}
aria-pressed={person.gender === "male"}
title={person.gender === "male" ? "Male — click to clear" : "Set male"}
className={`px-3 py-1 leading-none transition-colors ${
person.gender === "male"
? "bg-[rgb(120,159,172)] text-white"
: "text-[var(--muted)] hover:bg-bronze/[0.07]"
}`}
>
</button>
<button
type="button"
onClick={() => setGender(person.gender === "female" ? null : "female")}
aria-pressed={person.gender === "female"}
title={person.gender === "female" ? "Female — click to clear" : "Set female"}
className={`border-l border-[var(--border)] px-3 py-1 leading-none transition-colors ${
person.gender === "female"
? "bg-[rgb(196,138,146)] text-white"
: "text-[var(--muted)] hover:bg-bronze/[0.07]"
}`}
>
</button>
</span>
{isSelf && (
<span className="rounded-full bg-bronze/15 px-2.5 py-1 text-xs font-medium text-bronze">