From 69cea82e51b285147864154ae77290b57468fa42 Mon Sep 17 00:00:00 2001 From: Anaru Date: Tue, 25 Nov 2025 18:40:14 +0100 Subject: [PATCH] feat: Implement a new global, hierarchical tree view with SVG connections and dedicated styling. --- app.html | 18 ----- script.js | 207 ++++++++++++++++++++++++++++++++++++++++-------------- style.css | 27 +++++-- tree.css | 65 +++++++++++++++++ 4 files changed, 241 insertions(+), 76 deletions(-) create mode 100644 tree.css diff --git a/app.html b/app.html index 6808192..c0a0e67 100644 --- a/app.html +++ b/app.html @@ -49,24 +49,6 @@ - - -
- - -
-
-
-
-
-
-
-
-
- - - diff --git a/script.js b/script.js index 59dca8b..ce46c11 100644 --- a/script.js +++ b/script.js @@ -93,67 +93,184 @@ function setupTabs() { } // --- TREE RENDERER --- +// --- TREE RENDERER (GLOBAL) --- function renderTree() { - const parentsRow = document.getElementById('row-parents'); - const focusRow = document.getElementById('row-focus'); - const childrenRow = document.getElementById('row-children'); + const grid = document.getElementById('tree-grid'); + const svg = document.getElementById('tree-svg'); + if (!grid || !svg) return; - if (!parentsRow || !focusRow || !childrenRow) return; - - parentsRow.innerHTML = ''; - focusRow.innerHTML = ''; - childrenRow.innerHTML = ''; + grid.innerHTML = ''; + svg.innerHTML = ''; // Clear lines if (state.people.length === 0) { - focusRow.innerHTML = `
+ grid.innerHTML = `

Ingen personer i treet ennå.

Klikk på nede til høyre for å starte!

`; return; } - const focusPerson = state.people.find(p => p.id === state.focusId); - if (!focusPerson) return; + const visited = new Set(); + const roots = findRoots(); - // 1. Parents - const father = state.people.find(p => p.id === focusPerson.fatherId); - const mother = state.people.find(p => p.id === focusPerson.motherId); - parentsRow.appendChild(createSlot(father, 'Far', 'add-parent-m')); - parentsRow.appendChild(createSlot(mother, 'Mor', 'add-parent-f')); + roots.forEach(root => { + if (!visited.has(root.id)) { + const treeNode = buildTreeNode(root, visited); + grid.appendChild(treeNode); + } + }); - // 2. Partner (Left of Focus) - const partner = state.people.find(p => p.id === focusPerson.partnerId); - if (partner) { - const pCard = createCard(partner); - pCard.onclick = () => setFocus(partner.id); - focusRow.appendChild(pCard); + // Draw lines after layout is computed + setTimeout(drawConnections, 100); + window.addEventListener('resize', drawConnections); +} - // Heart Icon - const heart = document.createElement('div'); - heart.innerHTML = ''; - focusRow.appendChild(heart); +function findRoots() { + // A root is someone whose parents are NOT in the current list + return state.people.filter(p => { + const father = state.people.find(parent => parent.id === p.fatherId); + const mother = state.people.find(parent => parent.id === p.motherId); + return !father && !mother; + }); +} + +function buildTreeNode(person, visited) { + visited.add(person.id); + + // Container + const container = document.createElement('div'); + container.className = 'node-container'; + + // 1. Couple Wrapper + const coupleWrapper = document.createElement('div'); + coupleWrapper.className = 'couple-wrapper'; + + // Person Card + const pCard = createCard(person); + pCard.onclick = () => setFocus(person.id); // Keep focus logic for editing + coupleWrapper.appendChild(pCard); + + // Partner Card + if (person.partnerId) { + const partner = state.people.find(p => p.id === person.partnerId); + if (partner && !visited.has(partner.id)) { + visited.add(partner.id); + const partCard = createCard(partner); + partCard.onclick = () => setFocus(partner.id); + coupleWrapper.appendChild(partCard); + } + } + container.appendChild(coupleWrapper); + + // 2. Children + // Find children where this person (or partner) is a parent + const children = state.people.filter(p => + p.fatherId === person.id || p.motherId === person.id || + (person.partnerId && (p.fatherId === person.partnerId || p.motherId === person.partnerId)) + ); + + if (children.length > 0) { + const childrenContainer = document.createElement('div'); + childrenContainer.className = 'children-container'; + + // Sort children by birth date + children.sort((a, b) => (a.birthDate || '9999') > (b.birthDate || '9999') ? 1 : -1); + + children.forEach(child => { + if (!visited.has(child.id)) { + const childNode = buildTreeNode(child, visited); + childrenContainer.appendChild(childNode); + } + }); + container.appendChild(childrenContainer); } - // 3. Focus Person - focusRow.appendChild(createCard(focusPerson, true)); + return container; +} +function drawConnections() { + const svg = document.getElementById('tree-svg'); + const grid = document.getElementById('tree-grid'); + if (!svg || !grid) return; - // 4. Children - const children = state.people.filter(p => p.fatherId === state.focusId || p.motherId === state.focusId); - children.forEach(child => { - const cCard = createCard(child); - cCard.onclick = () => setFocus(child.id); - childrenRow.appendChild(cCard); + svg.innerHTML = ''; + const svgRect = svg.getBoundingClientRect(); + + // Map all visible cards by ID + const cardMap = new Map(); + document.querySelectorAll('.card').forEach(card => { + if (card.dataset.id) cardMap.set(card.dataset.id, card); }); + + const drawnPartners = new Set(); + + // Helper for coordinates + const getCenter = (el) => { + const rect = el.getBoundingClientRect(); + return { + x: rect.left + rect.width / 2 - svgRect.left, + y: rect.top + rect.height / 2 - svgRect.top, + bottom: rect.bottom - svgRect.top, + top: rect.top - svgRect.top + }; + }; + + state.people.forEach(p => { + const pCard = cardMap.get(p.id); + if (!pCard) return; + + const pCenter = getCenter(pCard); + + // 1. Parent Connections + [p.fatherId, p.motherId].forEach(parentId => { + if (parentId) { + const parentCard = cardMap.get(parentId); + if (parentCard) { + const parCenter = getCenter(parentCard); + // Draw Curved Line + // From Parent Bottom to Child Top + const path = `M ${parCenter.x} ${parCenter.bottom} + C ${parCenter.x} ${parCenter.bottom + 50}, + ${pCenter.x} ${pCenter.top - 50}, + ${pCenter.x} ${pCenter.top}`; + createPath(svg, path); + } + } + }); + + // 2. Partner Connection + if (p.partnerId) { + const partnerCard = cardMap.get(p.partnerId); + if (partnerCard) { + // Avoid drawing twice + const pairId = [p.id, p.partnerId].sort().join('-'); + if (!drawnPartners.has(pairId)) { + drawnPartners.add(pairId); + const partCenter = getCenter(partnerCard); + createPath(svg, `M ${pCenter.x} ${pCenter.y} L ${partCenter.x} ${partCenter.y}`); + } + } + } + }); +} + +function createPath(svg, d) { + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", d); + path.setAttribute("class", "connector"); + svg.appendChild(path); } function setFocus(id) { state.focusId = id; + document.querySelectorAll('.card').forEach(c => c.classList.remove('focus')); renderTree(); } -function createCard(person, isFocus = false) { +function createCard(person) { + const isFocus = (person.id === state.focusId); const el = document.createElement('div'); el.className = `card ${isFocus ? 'focus' : ''} ${person.gender === 'F' ? 'female' : 'male'}`; + el.dataset.id = person.id; // Crucial for connections const year = person.birthDate ? new Date(person.birthDate).getFullYear() : ''; const dead = person.deathDate ? '✝ ' : ''; @@ -166,6 +283,7 @@ function createCard(person, isFocus = false) { ${loc ? `
${loc}
` : ''} `; + // Always show actions if focused if (isFocus) { const actions = document.createElement('div'); actions.className = 'mini-actions'; @@ -193,24 +311,11 @@ function createCard(person, isFocus = false) { } function createSlot(person, label, action) { - const wrapper = document.createElement('div'); - wrapper.className = 'slot'; - wrapper.innerHTML = `${label}`; - - if (person) { - const card = createCard(person); - card.onclick = () => setFocus(person.id); - wrapper.appendChild(card); - } else { - const btn = document.createElement('div'); - btn.className = 'btn-slot'; - btn.innerHTML = ''; - btn.onclick = () => window.openModal(action); - wrapper.appendChild(btn); - } - return wrapper; + // Deprecated in global view, but kept if needed for other views + return document.createElement('div'); } + // --- MAP RENDERER --- function renderMap() { if (!state.map) { diff --git a/style.css b/style.css index 9438d76..2d41616 100644 --- a/style.css +++ b/style.css @@ -1,5 +1,6 @@ /* --- RØSSTAD DESIGN SYSTEM --- */ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap'); +@import url('tree.css'); :root { /* --- PALETTE: Røsstad Green --- */ @@ -567,17 +568,29 @@ body.login-page { display: flex; gap: 1rem; } + /* --- FAB (Floating Action Button) --- */ .fab-btn { - position: fixed; bottom: 2rem; right: 2rem; - width: 64px; height: 64px; border-radius: 50%; - background: var(--color-primary); color: white; - border: none; box-shadow: var(--shadow-lg); - display: flex; justify-content: center; align-items: center; - font-size: 2rem; cursor: pointer; z-index: 500; + position: fixed; + bottom: 2rem; + right: 2rem; + width: 64px; + height: 64px; + border-radius: 50%; + background: var(--color-primary); + color: white; + border: none; + box-shadow: var(--shadow-lg); + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + cursor: pointer; + z-index: 500; transition: transform 0.2s, background 0.2s; } + .fab-btn:hover { transform: scale(1.1) rotate(90deg); background: var(--color-primary-dark); -} +} \ No newline at end of file diff --git a/tree.css b/tree.css new file mode 100644 index 0000000..39d8913 --- /dev/null +++ b/tree.css @@ -0,0 +1,65 @@ +/* --- GLOBAL TREE LAYOUT --- */ +.tree-grid { + /* Override previous grid styles */ + display: flex !important; + flex-direction: row !important; + align-items: flex-start !important; + justify-content: center !important; + gap: 4rem !important; + padding-top: 2rem; + transform-origin: top center; +} + +.node-container { + display: flex; + flex-direction: column; + align-items: center; + margin: 0 1rem; + position: relative; + z-index: 1; +} + +.couple-wrapper { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + position: relative; + z-index: 2; +} + +.children-container { + display: flex; + flex-direction: row; + justify-content: center; + gap: 1rem; + position: relative; +} + +/* Connector Lines (SVG) */ +#tree-svg { + overflow: visible; +} + +path.connector { + fill: none; + stroke: #cbd5e1; + stroke-width: 2px; + stroke-linecap: round; + transition: stroke 0.3s; +} + +path.connector:hover { + stroke: var(--color-primary); + stroke-width: 3px; +} + +/* Adjust Card for Tree */ +.card { + width: 180px; /* Slightly smaller */ + min-height: 110px; + font-size: 0.9rem; +} + +.card-avatar { + font-size: 1.5rem; +}