diff --git a/gerbil-manager-web/e2e/stammbaum.spec.ts b/gerbil-manager-web/e2e/stammbaum.spec.ts
index db9da06..2f55793 100644
--- a/gerbil-manager-web/e2e/stammbaum.spec.ts
+++ b/gerbil-manager-web/e2e/stammbaum.spec.ts
@@ -110,3 +110,30 @@ test('+-Knopf ist sichtbar und lädt weitere Vorfahren nach (STAMMBAUM-EXPAND)
await expandBtn.click({ force: true })
await expect(page.getByRole('link', { name: 'Max' })).toBeVisible({ timeout: 8000 })
})
+
+test('Würfe-Panel zeigt Würfe des Wurzeltiers + Link öffnet Wurf (STAMMBAUM-LITTERS)', async ({ page }) => {
+ skipUnlessMock()
+ // Fridolin ist Vater von Wurf K (5 Junge) — Panel muss erscheinen.
+ await page.goto('/rennmaeuse/fridolin/stammbaum')
+ await expect(page.locator('.pedigree-card').first()).toBeVisible()
+
+ const panel = page.locator('.stammbaum-litters-panel')
+ await expect(panel).toBeVisible()
+ await expect(panel).toContainText(t.littersTitle)
+ await expect(panel).toContainText('Wurf K')
+ await expect(panel).toContainText('5')
+
+ // Link-Klick → Wurf-Detailseite
+ const wurfLink = panel.getByRole('link', { name: /Wurf K/ })
+ await expect(wurfLink).toBeVisible()
+ await wurfLink.click()
+ await expect(page).toHaveURL(/\/wuerfe\/w-kruemel/)
+})
+
+test('Kein Würfe-Panel wenn Wurzeltier keine Würfe hat (STAMMBAUM-LITTERS)', async ({ page }) => {
+ skipUnlessMock()
+ // Krümel hat noch keine Würfe als Elternteil → Panel muss fehlen.
+ await page.goto('/rennmaeuse/kruemel/stammbaum')
+ await expect(page.locator('.pedigree-card').first()).toBeVisible()
+ await expect(page.locator('.stammbaum-litters-panel')).not.toBeVisible()
+})
diff --git a/gerbil-manager-web/src/pages/StammbaumPage.tsx b/gerbil-manager-web/src/pages/StammbaumPage.tsx
index 8f93b01..9e511c0 100644
--- a/gerbil-manager-web/src/pages/StammbaumPage.tsx
+++ b/gerbil-manager-web/src/pages/StammbaumPage.tsx
@@ -22,9 +22,10 @@ import Tree from 'react-d3-tree'
import type { CustomNodeElementProps, Point, RawNodeDatum } from 'react-d3-tree'
import { de } from '../strings/de'
import { ApiError } from '../api/client'
+import { listLitters } from '../api/litters'
import { listColorVarieties } from '../api/lookups'
import { getInbreedingCoefficient } from '../api/pedigree'
-import type { Gender, Gerbil } from '../api/types'
+import type { Gender, Gerbil, Litter } from '../api/types'
import { useApi } from '../hooks/useApi'
import { formatDate, genderLabel } from '../format/labels'
import { UNKNOWN_FARBSCHLAG, fromDisplayString, genotypeToFarbschlag } from '../genetics'
@@ -156,6 +157,13 @@ export default function StammbaumPage() {
? `${(inbreeding.data * 100).toLocaleString('de-DE', { maximumFractionDigits: 1 })} %`
: t.inbreeding.unavailable
+ /* ── Würfe des Wurzeltiers (STAMMBAUM-LITTERS): aktualisiert bei Umwurzeln ── */
+ const rootLitters = useApi(
+ () => listLitters({ filter: `fatherId=${id}|motherId=${id}`, orderBy: 'date desc', pageSize: 50 }),
+ [id],
+ )
+ const rootLitterItems = rootLitters.data?.items ?? []
+
/* ── react-d3-tree-Daten ── */
const nodesByPath = useMemo(() => (root ? collectNodes(root) : null), [root])
const datum = useMemo(() => (root ? toRawNodeDatum(root, t.unknown) : null), [root, t])
@@ -315,25 +323,30 @@ export default function StammbaumPage() {
-
- {view && (
-
+
+ {rootLitterItems.length > 0 && (
+
)}
+
+ {view && (
+
+ )}
+
- {t.tapHint}
@@ -417,6 +430,36 @@ function PedigreeCard({
)
}
+/* ── Würfe-Panel (STAMMBAUM-LITTERS) ─────────────────────────────── */
+
+function LittersPanel({
+ litters,
+ t,
+}: {
+ litters: Litter[]
+ t: { littersTitle: string; littersJunge: string }
+}) {
+ return (
+
+ )
+}
+
function SexIcon({ gender }: { gender: Gender }) {
const symbol = gender === 'male' ? '♂' : gender === 'female' ? '♀' : '?'
return (
diff --git a/gerbil-manager-web/src/pages/stammbaum.css b/gerbil-manager-web/src/pages/stammbaum.css
index d03692c..4814943 100644
--- a/gerbil-manager-web/src/pages/stammbaum.css
+++ b/gerbil-manager-web/src/pages/stammbaum.css
@@ -45,9 +45,132 @@
color: var(--color-text-muted);
}
+/* ── Würfe-Panel + Layout (STAMMBAUM-LITTERS) ────────────────── */
+
+.stammbaum-layout {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+}
+
+/* Desktop: Würfe-Panel links vom Baum. */
+.stammbaum-litters-panel {
+ flex: none;
+ width: 148px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 0.35rem;
+ padding: 0.5rem 0.75rem 0.5rem 0;
+ border-right: 1px solid var(--color-border);
+ margin-right: 0;
+}
+
+.stammbaum-litters-panel__title {
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--color-text-muted);
+}
+
+.stammbaum-litters-panel__list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+ overflow-y: auto;
+ max-height: calc(clamp(18rem, 62dvh, 46rem) - 3rem);
+}
+
+.stammbaum-litters-panel__link {
+ display: flex;
+ flex-direction: column;
+ gap: 0.1rem;
+ padding: 0.3rem 0.45rem;
+ border-radius: 0.4rem;
+ text-decoration: none;
+ color: inherit;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ font-size: 0.8rem;
+ font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+}
+
+.stammbaum-litters-panel__link:hover {
+ background: var(--color-accent-soft);
+ border-color: var(--color-accent);
+ color: var(--color-accent);
+}
+
+.stammbaum-litters-panel__name {
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.stammbaum-litters-panel__born {
+ color: var(--color-text-muted);
+ font-size: 0.72rem;
+}
+
+/* Mobil (≤520px): Panel als kompakter horizontaler Streifen ÜBER dem Baum. */
+@media (max-width: 520px) {
+ .stammbaum-layout {
+ flex-direction: column;
+ }
+
+ .stammbaum-litters-panel {
+ width: auto;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 0.5rem;
+ padding: 0.4rem 0.5rem;
+ border-right: none;
+ border-bottom: 1px solid var(--color-border);
+ overflow-x: auto;
+ }
+
+ .stammbaum-litters-panel__title {
+ flex: none;
+ white-space: nowrap;
+ }
+
+ .stammbaum-litters-panel__list {
+ flex-direction: row;
+ flex-wrap: nowrap;
+ max-height: none;
+ overflow-x: auto;
+ overflow-y: hidden;
+ gap: 0.4rem;
+ }
+
+ .stammbaum-litters-panel__link {
+ flex-direction: row;
+ align-items: center;
+ gap: 0.35rem;
+ white-space: nowrap;
+ }
+
+ .stammbaum-litters-panel__born {
+ color: var(--color-text-muted);
+ }
+
+ /* Canvas behält explizite Höhe in column-Richtung. */
+ .stammbaum-canvas {
+ flex: 0 0 auto;
+ }
+}
+
/* ── Zeichenfläche ────────────────────────────────────────────── */
.stammbaum-canvas {
+ flex: 1 1 auto;
+ min-width: 0;
height: clamp(18rem, 62dvh, 46rem);
border: 1px solid var(--color-border);
border-radius: 0.6rem;
diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts
index 1e6e40a..6b599d1 100644
--- a/gerbil-manager-web/src/strings/de.ts
+++ b/gerbil-manager-web/src/strings/de.ts
@@ -358,6 +358,9 @@ export const de = {
/** Mini-Legende unter dem Baum (STAMMBAUM-EXPAND). */
hintName: 'Namenslink: Tierakte öffnen',
hintExpand: '+: weitere Vorfahren nachladen',
+ /** Würfe-Panel links (STAMMBAUM-LITTERS). */
+ littersTitle: 'Würfe',
+ littersJunge: 'Junge',
zoomIn: 'Vergrößern',
zoomOut: 'Verkleinern',
zoomFit: 'Ansicht einpassen',