From 69cea82e51b285147864154ae77290b57468fa42 Mon Sep 17 00:00:00 2001 From: Anaru Date: Tue, 25 Nov 2025 18:40:14 +0100 Subject: [PATCH 1/5] 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; +} From 60fa0dbcb0f3fcedf17f22f4a52c4b9c1542dd03 Mon Sep 17 00:00:00 2001 From: Anaru Date: Tue, 25 Nov 2025 18:47:08 +0100 Subject: [PATCH 2/5] refactor: Remove unused CSS variables, global styles, utility classes, and specific component styling. --- style.css | 63 ------------------------------------------------------- 1 file changed, 63 deletions(-) diff --git a/style.css b/style.css index 2d41616..ec91c73 100644 --- a/style.css +++ b/style.css @@ -35,54 +35,6 @@ /* --- SHADOWS & EFFECTS --- */ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); - --blur-glass: blur(12px); - - /* --- SPACING & RADIUS --- */ - --radius-sm: 0.5rem; - --radius-md: 0.75rem; - --radius-lg: 1.5rem; -} - -/* --- GLOBAL RESET --- */ -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: 'Outfit', sans-serif; - background-color: var(--color-bg); - color: var(--color-text-main); - height: 100vh; - overflow: hidden; - /* Subtle background pattern */ - background-image: - radial-gradient(at 0% 0%, rgba(15, 47, 36, 0.05) 0px, transparent 50%), - radial-gradient(at 100% 100%, rgba(217, 119, 6, 0.05) 0px, transparent 50%); -} - -/* --- UTILITIES --- */ -.hidden { - display: none !important; -} - -.full-width { - width: 100%; -} - -.flex-center { - display: flex; - justify-content: center; - align-items: center; -} - -/* --- LOGIN PAGE --- */ -body.login-page { - display: flex; - justify-content: center; - align-items: center; - background: linear-gradient(135deg, var(--color-primary-dark) 0%, var(--color-primary) 100%); } .login-container { @@ -449,21 +401,6 @@ body.login-page { .btn-slot:hover { border-color: var(--color-primary); - color: var(--color-primary); - background: white; -} - -/* Map & Timeline */ -#map-container { - width: 100%; - height: 100%; - background: #e2e8f0; -} - -.timeline-wrapper { - max-width: 800px; - margin: 0 auto; - padding: 3rem 1rem; overflow-y: auto; height: 100%; } From 7a8e8ebc49a13616c46b8f5cbbe41e77d2ae3c07 Mon Sep 17 00:00:00 2001 From: Anaru Date: Tue, 25 Nov 2025 18:52:56 +0100 Subject: [PATCH 3/5] Remove unused color and shadow variables and login container styling. --- style.css | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/style.css b/style.css index ec91c73..1e6a2ff 100644 --- a/style.css +++ b/style.css @@ -27,22 +27,6 @@ --color-text-muted: #64748b; /* Slate 500 */ - --color-male: #0ea5e9; - /* Sky Blue */ - --color-female: #be123c; - /* Rose */ - - /* --- SHADOWS & EFFECTS --- */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); -} - -.login-container { - background: rgba(255, 255, 255, 0.1); - backdrop-filter: var(--blur-glass); - padding: 3rem; - border-radius: var(--radius-lg); - width: 100%; max-width: 400px; text-align: center; color: white; From 7b897d2e06a38d676f2ee48b5842abc535e11a9e Mon Sep 17 00:00:00 2001 From: Anaru Date: Tue, 25 Nov 2025 19:05:18 +0100 Subject: [PATCH 4/5] Revert "Rename Script.js to script.js to fix case sensitivity on GitHub Pages" This reverts commit 759ee25150218b43d59fccb6c1af955f0be3d39b. --- script.js => Script.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename script.js => Script.js (100%) diff --git a/script.js b/Script.js similarity index 100% rename from script.js rename to Script.js From 4a703998f5d9c27ec3fe5d9d7aea9e1400d88f38 Mon Sep 17 00:00:00 2001 From: Anaru Date: Tue, 25 Nov 2025 19:05:40 +0100 Subject: [PATCH 5/5] refactor: Remove dedicated tree layout styles by deleting `tree.css`. --- style.css | 1 + tree.css | 65 ------------------------------------------------------- 2 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 tree.css diff --git a/style.css b/style.css index 1e6a2ff..417af22 100644 --- a/style.css +++ b/style.css @@ -478,6 +478,7 @@ margin-bottom: 1rem; } + .form-label { display: block; font-size: 0.85rem; diff --git a/tree.css b/tree.css deleted file mode 100644 index 39d8913..0000000 --- a/tree.css +++ /dev/null @@ -1,65 +0,0 @@ -/* --- 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; -}