PHASE 1 — Sofort-Cleanup ohne Risiko: - Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen: * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3 * flex-between, flex-1-min, mb-1/3, mt-1/3 * icon-xs/sm/md/lg, label-block, caption - index.html bindet utilities.css ein - mb-3/mt-3 ergänzt (waren in design-system.css unvollständig) PHASE 2 — .by-tab Modifier für Vereinheitlichung: - .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.) - .by-tabs.sticky (Desktop vertikale Tabs für Admin) - .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll) - .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border) PHASE 3 — Inline-Style → Klassen-Migration (Python-Script): - 948 Inline-Styles entfernt (5101 → 4153, -18%) - 962 Migrationen über 47 Page-Dateien - Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67), litters.js (62), settings.js (61), zuchthunde.js (51) - Patterns: text-muted, text-secondary, text-danger, text-xs-muted, text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3, p-3/4, mb-2/3/4, hidden, w-full, flex-1, ... - Bewahrt bestehende class-Attribute (mergt korrekt) Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
718 lines
23 KiB
JavaScript
718 lines
23 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — Zucht-Profil
|
|
Vollständiges Profil eines Zuchthundes:
|
|
Basisdaten + Stammbaum (4 Generationen) + Gesundheitstests
|
|
+ Gentests + Titel.
|
|
============================================================ */
|
|
|
|
window.Page_zucht_profil = (() => {
|
|
|
|
let _container = null;
|
|
let _appState = null;
|
|
let _hundId = null;
|
|
let _hund = null;
|
|
|
|
// ----------------------------------------------------------
|
|
// Hilfsfunktionen
|
|
// ----------------------------------------------------------
|
|
function _esc(s) {
|
|
if (!s) return '';
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function _fmtDate(iso) {
|
|
if (!iso) return '—';
|
|
const d = new Date(iso + 'T12:00:00');
|
|
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Badge-Farben
|
|
// ----------------------------------------------------------
|
|
function _healthBadge(testTyp, ergebnis) {
|
|
const e = (ergebnis || '').trim().toUpperCase();
|
|
let color = '#6B7280';
|
|
|
|
if (testTyp === 'HD') {
|
|
if (['A1', 'A2', 'A'].includes(e)) color = '#22C55E';
|
|
else if (['B1', 'B2', 'B'].includes(e)) color = '#86EFAC';
|
|
else if (e === 'C') color = '#EAB308';
|
|
else if (e === 'D') color = '#F97316';
|
|
else if (e === 'E') color = '#EF4444';
|
|
} else if (testTyp === 'ED') {
|
|
if (e === '0' || e === 'ED 0') color = '#22C55E';
|
|
else if (e === '1' || e === 'ED 1') color = '#EAB308';
|
|
else if (e === '2' || e === 'ED 2') color = '#F97316';
|
|
else if (e === '3' || e === 'ED 3') color = '#EF4444';
|
|
} else {
|
|
const el = e.toLowerCase();
|
|
if (el === 'clear') color = '#22C55E';
|
|
if (el === 'carrier') color = '#EAB308';
|
|
if (el === 'affected') color = '#EF4444';
|
|
}
|
|
|
|
return `<span class="zp-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`;
|
|
}
|
|
|
|
function _geneticBadge(ergebnis) {
|
|
const e = (ergebnis || '').toLowerCase().trim();
|
|
let color = '#6B7280';
|
|
if (e === 'clear') color = '#22C55E';
|
|
if (e === 'carrier') color = '#F59E0B';
|
|
if (e === 'affected') color = '#EF4444';
|
|
return `<span class="zp-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`;
|
|
}
|
|
|
|
function _titleTypBadge(typ) {
|
|
const t = (typ || '').toLowerCase();
|
|
const colors = {
|
|
ausstellung: '#8B5CF6',
|
|
arbeit: '#F59E0B',
|
|
champion: '#EF4444',
|
|
sport: '#3B82F6',
|
|
zucht: '#10B981',
|
|
};
|
|
const color = colors[t] || '#6B7280';
|
|
return `<span class="zp-badge" style="background:${color}">${_esc(typ || '—')}</span>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// INIT / LIFECYCLE
|
|
// ----------------------------------------------------------
|
|
async function init(container, appState, params) {
|
|
_container = container;
|
|
_appState = appState;
|
|
_hundId = params?.id ? parseInt(params.id) : null;
|
|
|
|
if (!_hundId) {
|
|
_container.innerHTML = `
|
|
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
|
|
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('warning')}</div>
|
|
<p class="text-secondary">Kein Hund angegeben.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
_renderSkeleton();
|
|
await _load();
|
|
}
|
|
|
|
function refresh() {
|
|
if (_hundId) _load();
|
|
}
|
|
|
|
function onDogChange() {}
|
|
|
|
// ----------------------------------------------------------
|
|
// Daten laden
|
|
// ----------------------------------------------------------
|
|
async function _load() {
|
|
try {
|
|
const [hund, tree, health, genetic, titles] = await Promise.all([
|
|
API.zuchthunde.get(_hundId),
|
|
API.zuchthunde.pedigree(_hundId, 4),
|
|
API.zuchthunde.healthTests(_hundId),
|
|
API.zuchthunde.geneticTests(_hundId),
|
|
API.zuchthunde.titles(_hundId),
|
|
]);
|
|
_hund = hund;
|
|
_renderAll(hund, tree, health, genetic, titles);
|
|
} catch (err) {
|
|
_container.innerHTML = `
|
|
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
|
|
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('warning')}</div>
|
|
<p class="text-danger">${_esc(err.message || 'Fehler beim Laden.')}</p>
|
|
<button class="btn btn-secondary" onclick="history.back()">Zurück</button>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Skeleton während des Ladens
|
|
// ----------------------------------------------------------
|
|
function _renderSkeleton() {
|
|
_container.innerHTML = `
|
|
<div class="zp-layout">
|
|
<button class="btn btn-ghost btn-sm zp-back-btn mb-4">
|
|
${UI.icon('arrow-left')} Zurück zur Zuchtkartei
|
|
</button>
|
|
${UI.skeleton(6)}
|
|
</div>`;
|
|
_container.querySelector('.zp-back-btn')?.addEventListener('click', () => {
|
|
if (window.history.length > 1) history.back();
|
|
else App.navigate('zuchthunde');
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Vollständige Seite rendern
|
|
// ----------------------------------------------------------
|
|
function _renderAll(hund, tree, health, genetic, titles) {
|
|
_container.innerHTML = `
|
|
<div class="zp-layout">
|
|
|
|
<!-- Zurück + Link teilen -->
|
|
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-4)">
|
|
<button class="btn btn-ghost btn-sm zp-back-btn">
|
|
${UI.icon('arrow-left')} Zurück zur Zuchtkartei
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm zp-share-btn" title="Link teilen">
|
|
${UI.icon('link-simple')} Link teilen
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
${_renderHeader(hund)}
|
|
|
|
<!-- Stammbaum -->
|
|
<div class="zp-section">
|
|
<h3 class="zp-section-title">${UI.icon('tree-structure')} Stammbaum</h3>
|
|
<div class="zp-pedigree-wrap">
|
|
${_renderPedigree(tree, 4)}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gesundheitstests -->
|
|
<div class="zp-section">
|
|
<h3 class="zp-section-title">${UI.icon('heart')} Gesundheitstests</h3>
|
|
${_renderHealthTable(health)}
|
|
</div>
|
|
|
|
<!-- Gentests -->
|
|
<div class="zp-section">
|
|
<h3 class="zp-section-title">${UI.icon('dna')} Gentests</h3>
|
|
${_renderGeneticTable(genetic)}
|
|
</div>
|
|
|
|
<!-- Titel -->
|
|
<div class="zp-section">
|
|
<h3 class="zp-section-title">${UI.icon('trophy')} Titel & Auszeichnungen</h3>
|
|
${_renderTitlesList(titles)}
|
|
</div>
|
|
|
|
</div>`;
|
|
|
|
// Zurück-Button verdrahten
|
|
_container.querySelector('.zp-back-btn')?.addEventListener('click', () => {
|
|
if (window.history.length > 1) history.back();
|
|
else App.navigate('zuchthunde');
|
|
});
|
|
|
|
// Link teilen
|
|
_container.querySelector('.zp-share-btn')?.addEventListener('click', () => {
|
|
const url = window.location.origin + '#zucht-profil&id=' + _hundId;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
UI.toast.success('Link kopiert!');
|
|
}).catch(() => {
|
|
UI.toast.error('Kopieren nicht möglich.');
|
|
});
|
|
});
|
|
|
|
// Stammbaum-Klicks verdrahten (außer Gen 1 = Proband selbst)
|
|
_container.querySelectorAll('.pedigree-cell[data-hund-id]').forEach(cell => {
|
|
const nodeId = parseInt(cell.dataset.hundId);
|
|
const gen = parseInt(cell.dataset.gen || '1');
|
|
if (gen === 1) return; // Proband — kein Klick nötig
|
|
cell.style.cursor = 'pointer';
|
|
cell.addEventListener('click', () => {
|
|
App.navigate('zucht-profil', true, { id: nodeId });
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Header
|
|
// ----------------------------------------------------------
|
|
function _renderHeader(h) {
|
|
const gIcon = h.geschlecht === 'maennlich' ? UI.icon('gender-male') :
|
|
h.geschlecht === 'weiblich' ? UI.icon('gender-female') : UI.icon('dog');
|
|
|
|
const geburtsjahrLabel = h.geburtsdatum
|
|
? `*${new Date(h.geburtsdatum + 'T12:00:00').toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}`
|
|
: null;
|
|
|
|
const geschlechtLabel = h.geschlecht === 'maennlich' ? 'Rüde' :
|
|
h.geschlecht === 'weiblich' ? 'Hündin' : null;
|
|
|
|
const metaItems = [
|
|
h.rasse ? `${UI.icon('paw-print')} ${_esc(h.rasse)}` : null,
|
|
geschlechtLabel ? `${gIcon} ${geschlechtLabel}` : null,
|
|
geburtsjahrLabel ? `${UI.icon('calendar-dots')} ${geburtsjahrLabel}` : null,
|
|
].filter(Boolean);
|
|
|
|
const identItems = [
|
|
h.chip_nr ? `${UI.icon('barcode')} Chip: ${_esc(h.chip_nr)}` : null,
|
|
h.zuchtbuchnummer ? `${UI.icon('book-open')} ZB-Nr.: ${_esc(h.zuchtbuchnummer)}` : null,
|
|
h.taetowier_nr ? `${UI.icon('pencil-simple')} Tätowierung: ${_esc(h.taetowier_nr)}` : null,
|
|
h.farbe ? `${UI.icon('palette')} ${_esc(h.farbe)}` : null,
|
|
].filter(Boolean);
|
|
|
|
const elternItems = [
|
|
h.vater_name ? `Vater: ${_esc(h.vater_name)}` : null,
|
|
h.mutter_name ? `Mutter: ${_esc(h.mutter_name)}` : null,
|
|
].filter(Boolean);
|
|
|
|
return `
|
|
<div class="zp-header">
|
|
<div class="zp-header-icon">${gIcon}</div>
|
|
<div class="zp-header-body">
|
|
<h2 class="zp-header-name">
|
|
${_esc(h.name)}
|
|
${h.rufname ? `<span class="zp-header-rufname">(${_esc(h.rufname)})</span>` : ''}
|
|
</h2>
|
|
${metaItems.length ? `
|
|
<div class="zp-header-meta">
|
|
${metaItems.map(m => `<span>${m}</span>`).join('<span class="zp-meta-sep">·</span>')}
|
|
</div>` : ''}
|
|
${identItems.length ? `
|
|
<div class="zp-header-meta zp-header-ident">
|
|
${identItems.map(m => `<span>${m}</span>`).join('')}
|
|
</div>` : ''}
|
|
${elternItems.length ? `
|
|
<div class="zp-header-meta text-xs-secondary">
|
|
${elternItems.join(' · ')}
|
|
</div>` : ''}
|
|
${h.notiz ? `<div class="zp-header-notiz">${_esc(h.notiz)}</div>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Stammbaum
|
|
// ----------------------------------------------------------
|
|
function _renderPedigree(tree, generations) {
|
|
const totalRows = Math.pow(2, generations - 1); // 8 für 4 Generationen
|
|
|
|
// Alle Knoten rekursiv einsammeln
|
|
function collect(node, gen, rowStart, rowSpan) {
|
|
if (gen > generations) return [];
|
|
const items = [{ node: node || null, gen, rowStart, rowSpan }];
|
|
if (gen < generations) {
|
|
const half = rowSpan / 2;
|
|
items.push(...collect(node?.vater || null, gen + 1, rowStart, half));
|
|
items.push(...collect(node?.mutter || null, gen + 1, rowStart + half, half));
|
|
}
|
|
return items;
|
|
}
|
|
|
|
const items = collect(tree, 1, 1, totalRows);
|
|
|
|
const cells = items.map(({ node, gen, rowStart, rowSpan }) => {
|
|
const isEmpty = !node;
|
|
return `
|
|
<div class="pedigree-cell ${isEmpty ? 'pedigree-empty' : ''}"
|
|
style="grid-column:${gen}; grid-row:${rowStart} / span ${rowSpan};
|
|
align-items:center; display:flex;"
|
|
data-gen="${gen}"
|
|
${node ? `data-hund-id="${node.id}"` : ''}>
|
|
${node ? _pedigreeNodeHTML(node, gen) : `<div class="pedigree-unknown">${UI.icon('question')}</div>`}
|
|
</div>`;
|
|
}).join('');
|
|
|
|
return `
|
|
<div class="pedigree-grid"
|
|
style="
|
|
display:grid;
|
|
grid-template-columns:repeat(${generations}, minmax(160px, 1fr));
|
|
grid-template-rows:repeat(${totalRows}, minmax(56px, auto));
|
|
gap:4px;
|
|
min-width:${generations * 170}px;
|
|
">
|
|
${cells}
|
|
</div>`;
|
|
}
|
|
|
|
function _pedigreeNodeHTML(node, gen) {
|
|
const gIcon = node.geschlecht === 'maennlich' ? UI.icon('gender-male') :
|
|
node.geschlecht === 'weiblich' ? UI.icon('gender-female') : '';
|
|
const dob = node.geburtsdatum
|
|
? `*${new Date(node.geburtsdatum + 'T12:00:00').getFullYear()}`
|
|
: '';
|
|
|
|
const isProband = gen === 1;
|
|
const bgColor = isProband ? 'var(--c-primary)' : 'var(--c-surface-2, var(--c-surface))';
|
|
const textColor = isProband ? '#fff' : 'var(--c-text)';
|
|
const borderColor = isProband ? 'var(--c-primary)' : 'var(--c-border)';
|
|
|
|
return `
|
|
<div class="pedigree-node pedigree-node--gen${gen}"
|
|
style="background:${bgColor};
|
|
color:${textColor};
|
|
border:1px solid ${borderColor};
|
|
border-radius:var(--radius-md);
|
|
padding:var(--space-2) var(--space-3);
|
|
width:100%;
|
|
box-sizing:border-box;">
|
|
<div style="font-weight:600;font-size:var(--text-sm);
|
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
|
${gIcon} ${_esc(node.name)}
|
|
</div>
|
|
${node.rufname
|
|
? `<div style="font-size:var(--text-xs);opacity:.75;white-space:nowrap;
|
|
overflow:hidden;text-overflow:ellipsis;">${_esc(node.rufname)}</div>`
|
|
: ''}
|
|
${dob
|
|
? `<div style="font-size:var(--text-xs);opacity:.65;">${dob}</div>`
|
|
: ''}
|
|
${node.zuchtbuchnummer
|
|
? `<div style="font-size:var(--text-xs);opacity:.55;white-space:nowrap;
|
|
overflow:hidden;text-overflow:ellipsis;">${_esc(node.zuchtbuchnummer)}</div>`
|
|
: ''}
|
|
</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Gesundheitstests-Tabelle
|
|
// ----------------------------------------------------------
|
|
function _renderHealthTable(tests) {
|
|
if (!tests || !tests.length) {
|
|
return `<p class="zp-empty">Noch keine Gesundheitstests eingetragen.</p>`;
|
|
}
|
|
|
|
const rows = tests.map(t => `
|
|
<tr>
|
|
<td class="zp-td">
|
|
<span style="font-weight:var(--weight-medium)">${_esc(t.test_typ || 'Sonstiges')}</span>
|
|
${t.test_name ? `<br><span class="text-xs-secondary">${_esc(t.test_name)}</span>` : ''}
|
|
</td>
|
|
<td class="zp-td">${_healthBadge(t.test_typ || '', t.ergebnis)}</td>
|
|
<td class="zp-td zp-td-muted">${t.untersuch_am ? _fmtDate(t.untersuch_am) : '—'}</td>
|
|
<td class="zp-td zp-td-muted">${t.labor ? _esc(t.labor) : '—'}</td>
|
|
</tr>`).join('');
|
|
|
|
return `
|
|
<div class="zp-table-wrap">
|
|
<table class="zp-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="zp-th">Test</th>
|
|
<th class="zp-th">Ergebnis</th>
|
|
<th class="zp-th">Datum</th>
|
|
<th class="zp-th">Labor / Institut</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Gentests-Tabelle
|
|
// ----------------------------------------------------------
|
|
function _renderGeneticTable(tests) {
|
|
if (!tests || !tests.length) {
|
|
return `<p class="zp-empty">Noch keine Gentests eingetragen.</p>`;
|
|
}
|
|
|
|
const rows = tests.map(t => `
|
|
<tr>
|
|
<td class="zp-td">
|
|
<span style="font-weight:var(--weight-medium)">${_esc(t.marker_name || '—')}</span>
|
|
</td>
|
|
<td class="zp-td">${_geneticBadge(t.ergebnis_klasse)}</td>
|
|
<td class="zp-td zp-td-muted">${t.getestet_am ? _fmtDate(t.getestet_am) : '—'}</td>
|
|
<td class="zp-td zp-td-muted">${t.labor ? _esc(t.labor) : '—'}</td>
|
|
</tr>`).join('');
|
|
|
|
return `
|
|
<div class="zp-table-wrap">
|
|
<table class="zp-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="zp-th">Marker / Gen</th>
|
|
<th class="zp-th">Ergebnis</th>
|
|
<th class="zp-th">Datum</th>
|
|
<th class="zp-th">Labor</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// Titel-Liste
|
|
// ----------------------------------------------------------
|
|
function _renderTitlesList(titles) {
|
|
if (!titles || !titles.length) {
|
|
return `<p class="zp-empty">Noch keine Titel eingetragen.</p>`;
|
|
}
|
|
|
|
// Chronologisch sortieren (neuestes zuerst)
|
|
const sorted = [...titles].sort((a, b) => {
|
|
const da = a.verliehen_am || '0000';
|
|
const db = b.verliehen_am || '0000';
|
|
return db.localeCompare(da);
|
|
});
|
|
|
|
const items = sorted.map(t => `
|
|
<div class="zp-title-item">
|
|
<div class="zp-title-badges">
|
|
${_titleTypBadge(t.titel_typ)}
|
|
${t.formwert
|
|
? `<span class="zp-badge" style="background:#3B82F6">${_esc(t.formwert)}</span>`
|
|
: ''}
|
|
</div>
|
|
<div class="zp-title-name">${_esc(t.titel_name || '—')}</div>
|
|
<div class="zp-title-meta">
|
|
${t.verliehen_am ? `${UI.icon('calendar-dots')} ${_fmtDate(t.verliehen_am)}` : ''}
|
|
${t.ort ? ` · ${UI.icon('map-pin')} ${_esc(t.ort)}` : ''}
|
|
${t.richter ? ` · ${UI.icon('user')} ${_esc(t.richter)}` : ''}
|
|
${t.ausstellung ? `<br><span class="text-xs">${UI.icon('ticket')} ${_esc(t.ausstellung)}</span>` : ''}
|
|
</div>
|
|
</div>`).join('');
|
|
|
|
return `<div class="zp-titles-list">${items}</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// CSS (einmalig injizieren)
|
|
// ----------------------------------------------------------
|
|
(function _injectStyles() {
|
|
if (document.getElementById('zp-styles')) return;
|
|
const s = document.createElement('style');
|
|
s.id = 'zp-styles';
|
|
s.textContent = `
|
|
|
|
/* Layout */
|
|
.zp-layout {
|
|
padding: var(--space-4) 0;
|
|
}
|
|
|
|
/* Header */
|
|
.zp-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--space-4);
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--space-4);
|
|
margin-bottom: var(--space-5);
|
|
}
|
|
|
|
.zp-header-icon {
|
|
font-size: 2rem;
|
|
flex-shrink: 0;
|
|
line-height: 1;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.zp-header-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.zp-header-name {
|
|
font-size: var(--text-xl);
|
|
font-weight: var(--weight-bold);
|
|
margin: 0 0 var(--space-1);
|
|
display: flex;
|
|
align-items: baseline;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.zp-header-rufname {
|
|
font-size: var(--text-base);
|
|
font-weight: var(--weight-normal);
|
|
color: var(--c-text-secondary);
|
|
}
|
|
|
|
.zp-header-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-sm);
|
|
color: var(--c-text-secondary);
|
|
margin-top: var(--space-1);
|
|
}
|
|
|
|
.zp-header-ident {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--space-1);
|
|
font-size: var(--text-xs);
|
|
margin-top: var(--space-2);
|
|
}
|
|
|
|
.zp-meta-sep {
|
|
opacity: .4;
|
|
}
|
|
|
|
.zp-header-notiz {
|
|
font-size: var(--text-xs);
|
|
color: var(--c-text-secondary);
|
|
font-style: italic;
|
|
margin-top: var(--space-2);
|
|
}
|
|
|
|
/* Sektion */
|
|
.zp-section {
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--space-4);
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.zp-section-title {
|
|
font-size: var(--text-base);
|
|
font-weight: var(--weight-semibold);
|
|
margin: 0 0 var(--space-4);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
/* Stammbaum-Wrapper: horizontal scrollbar auf Mobile */
|
|
.zp-pedigree-wrap {
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
padding-bottom: var(--space-2);
|
|
}
|
|
|
|
/* Stammbaum-Zellen */
|
|
.pedigree-cell {
|
|
box-sizing: border-box;
|
|
padding: 2px;
|
|
min-height: 56px;
|
|
}
|
|
|
|
.pedigree-cell:not(.pedigree-empty):hover .pedigree-node {
|
|
opacity: .85;
|
|
}
|
|
|
|
.pedigree-empty {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.pedigree-unknown {
|
|
width: 100%;
|
|
min-height: 52px;
|
|
border: 1px dashed var(--c-border);
|
|
border-radius: var(--radius-md);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--c-text-muted);
|
|
font-size: var(--text-lg);
|
|
opacity: .5;
|
|
}
|
|
|
|
/* Tabellen */
|
|
.zp-table-wrap {
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
border-radius: var(--radius-md);
|
|
border: 1px solid var(--c-border);
|
|
}
|
|
|
|
.zp-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: var(--text-sm);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.zp-th {
|
|
padding: var(--space-2) var(--space-3);
|
|
text-align: left;
|
|
font-size: var(--text-xs);
|
|
font-weight: var(--weight-semibold);
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
color: var(--c-text-secondary);
|
|
background: var(--c-surface-2, var(--c-bg));
|
|
border-bottom: 1px solid var(--c-border);
|
|
}
|
|
|
|
.zp-td {
|
|
padding: var(--space-2) var(--space-3);
|
|
border-bottom: 1px solid var(--c-border);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.zp-table tbody tr:last-child .zp-td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.zp-table tbody tr:hover {
|
|
background: var(--c-surface-2, var(--c-bg));
|
|
}
|
|
|
|
.zp-td-muted {
|
|
color: var(--c-text-secondary);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
/* Badge */
|
|
.zp-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 99px;
|
|
font-size: var(--text-xs);
|
|
font-weight: var(--weight-semibold);
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Titel-Liste */
|
|
.zp-titles-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.zp-title-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--space-3);
|
|
padding: var(--space-3);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--c-surface-2, var(--c-bg));
|
|
}
|
|
|
|
.zp-title-badges {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-1);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zp-title-name {
|
|
font-weight: var(--weight-semibold);
|
|
font-size: var(--text-sm);
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.zp-title-meta {
|
|
font-size: var(--text-xs);
|
|
color: var(--c-text-secondary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* Leer-Zustand */
|
|
.zp-empty {
|
|
color: var(--c-text-muted);
|
|
font-size: var(--text-sm);
|
|
margin: 0;
|
|
font-style: italic;
|
|
}
|
|
`;
|
|
document.head.appendChild(s);
|
|
})();
|
|
|
|
return { init, refresh, onDogChange };
|
|
|
|
})();
|