From ac040fa8bcafb511450622090cd67b268a9a8f01 Mon Sep 17 00:00:00 2001 From: Anaru Date: Tue, 25 Nov 2025 23:48:56 +0100 Subject: [PATCH] Remove all application source code, configuration, documentation, and styling. --- README.md | 26 --- Script.js | 559 ----------------------------------------------------- app.html | 166 ---------------- config.js | 9 - index.html | 37 ---- login.js | 92 --------- style.css | 490 +--------------------------------------------- 7 files changed, 1 insertion(+), 1378 deletions(-) delete mode 100644 README.md delete mode 100644 Script.js delete mode 100644 app.html delete mode 100644 config.js delete mode 100644 index.html delete mode 100644 login.js diff --git a/README.md b/README.md deleted file mode 100644 index 9a7490b..0000000 --- a/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Røsstad Familietre - -**Nettsiden finner du her:** [https://funnyeivske.github.io/Roesstad/](https://funnyeivske.github.io/Roesstad/) - -## Om Nettsiden -Dette er et digitalt samlingspunkt for Røsstad-familietreet. Nettsiden er designet for å bevare og visualisere familiens historie på en moderne og interaktiv måte. - -## Hvordan den fungerer - -### 1. Familietre (Hovedvisning) -Her ser du selve slektstreet. -- **Navigering:** Du kan dra i bakgrunnen for å flytte deg rundt, og zoome ut/inn. -- **Personkort:** Klikk på en person for å sette dem i fokus. -- **Legg til:** Bruk den store grønne knappen nede til høyre for å legge til nye familiemedlemmer. - -### 2. Kart -Se hvor familien har sine røtter. -- Når du legger til en person, kan du skrive inn et sted (f.eks. "Grimstad"). -- Personen vil da dukke opp som en markør på kartet. - -### 3. Tidslinje -En kronologisk oversikt over familiens historie. -- Her listes alle fødsler og dødsfall opp automatisk basert på informasjonen i familietreet. - ---- -*Laget med ❤️ for Røsstad-familien.* diff --git a/Script.js b/Script.js deleted file mode 100644 index d695afa..0000000 --- a/Script.js +++ /dev/null @@ -1,559 +0,0 @@ -import { initializeApp } from "https://www.gstatic.com/firebasejs/10.14.1/firebase-app.js"; -import { getAuth, onAuthStateChanged, signOut } from "https://www.gstatic.com/firebasejs/10.14.1/firebase-auth.js"; -import { getFirestore, collection, addDoc, updateDoc, doc, onSnapshot, query } from "https://www.gstatic.com/firebasejs/10.14.1/firebase-firestore.js"; -import { firebaseConfig } from "./config.js"; - -// --- INITIALIZATION --- -const app = initializeApp(firebaseConfig); -const auth = getAuth(app); -const db = getFirestore(app); - -// --- STATE --- -const state = { - user: null, - people: [], - focusId: null, - scale: 1.0, - modal: { - action: null, // 'add-child', 'add-parent-m', etc. - mode: 'new' // 'new' or 'existing' - }, - map: null -}; - -// --- IMMEDIATE SETUP (UI Handlers) --- -// This ensures buttons work even if Auth is slow -// --- IMMEDIATE SETUP (UI Handlers) --- -// This ensures buttons work even if Auth is slow -document.addEventListener('DOMContentLoaded', () => { - setupTabs(); - setupZoom(); - setupModal(); - setupHelp(); - setupFAB(); -}); - -// --- AUTHENTICATION --- -onAuthStateChanged(auth, (user) => { - if (user) { - state.user = user; - startDataListener(); - } else { - // Redirect to login if not authenticated - // Note: In local development with file://, this might trigger unexpectedly. - // For now, we assume index.html is the login page. - if (!window.location.pathname.endsWith('index.html') && !window.location.pathname.endsWith('/')) { - window.location.href = "index.html"; - } - } -}); - -document.getElementById('btn-logout')?.addEventListener('click', () => { - signOut(auth).then(() => window.location.href = "index.html"); -}); - -// --- DATA LISTENER --- -function startDataListener() { - const q = query(collection(db, 'familyMembers')); - onSnapshot(q, (snapshot) => { - state.people = snapshot.docs.map(d => ({ id: d.id, ...d.data() })); - - // Set initial focus if needed - if (!state.focusId && state.people.length > 0) { - const me = state.people.find(p => p.name && (p.name.toLowerCase().includes('eivind') || p.name.toLowerCase() === 'meg')); - state.focusId = me ? me.id : state.people[0].id; - } - - renderAll(); - }); -} - -function renderAll() { - renderTree(); - renderMap(); - renderTimeline(); -} - -// --- TAB NAVIGATION --- -function setupTabs() { - document.querySelectorAll('.nav-tab[data-view]').forEach(btn => { - btn.addEventListener('click', () => { - // Update Active Tab - document.querySelectorAll('.nav-tab[data-view]').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - - // Show View - document.querySelectorAll('.view-section').forEach(v => v.classList.add('hidden')); - const viewId = `view-${btn.dataset.view}`; - const viewEl = document.getElementById(viewId); - if (viewEl) viewEl.classList.remove('hidden'); - - // Special handling for Map resize - if (btn.dataset.view === 'map' && state.map) { - setTimeout(() => state.map.invalidateSize(), 100); - } - }); - }); -} - -// --- TREE RENDERER --- -// --- TREE RENDERER (GLOBAL) --- -function renderTree() { - const grid = document.getElementById('tree-grid'); - const svg = document.getElementById('tree-svg'); - if (!grid || !svg) return; - - grid.innerHTML = ''; - svg.innerHTML = ''; // Clear lines - - if (state.people.length === 0) { - grid.innerHTML = `
-

Ingen personer i treet ennå.

-

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

-
`; - return; - } - - const visited = new Set(); - const roots = findRoots(); - - roots.forEach(root => { - if (!visited.has(root.id)) { - const treeNode = buildTreeNode(root, visited); - grid.appendChild(treeNode); - } - }); - - // Draw lines after layout is computed - setTimeout(drawConnections, 100); - window.addEventListener('resize', drawConnections); -} - -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); - } - - return container; -} -function drawConnections() { - const svg = document.getElementById('tree-svg'); - const grid = document.getElementById('tree-grid'); - if (!svg || !grid) return; - - 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) { - 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 ? '✝ ' : ''; - const loc = person.location ? person.location.split(',')[0] : ''; - - el.innerHTML = ` - -
${person.name}
-
${dead}${year}
- ${loc ? `
${loc}
` : ''} - `; - - // Always show actions if focused - if (isFocus) { - const actions = document.createElement('div'); - actions.className = 'mini-actions'; - - // Add Child Button - const btnChild = document.createElement('button'); - btnChild.className = 'btn-mini btn-add-child'; - btnChild.innerHTML = ''; - btnChild.title = "Legg til barn"; - btnChild.onclick = (e) => { e.stopPropagation(); window.openModal('add-child'); }; - actions.appendChild(btnChild); - - // Add Partner Button (if none) - if (!person.partnerId) { - const btnPartner = document.createElement('button'); - btnPartner.className = 'btn-mini btn-add-partner'; - btnPartner.innerHTML = ''; - btnPartner.title = "Legg til partner"; - btnPartner.onclick = (e) => { e.stopPropagation(); window.openModal('add-partner'); }; - actions.appendChild(btnPartner); - } - el.appendChild(actions); - } - return el; -} - -function createSlot(person, label, action) { - // Deprecated in global view, but kept if needed for other views - return document.createElement('div'); -} - - -// --- MAP RENDERER --- -function renderMap() { - if (!state.map) { - const mapContainer = document.getElementById('map-container'); - if (!mapContainer) return; - - state.map = L.map('map-container').setView([65.0, 13.0], 5); - L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { - attribution: '© OpenStreetMap © CARTO', - subdomains: 'abcd', - maxZoom: 19 - }).addTo(state.map); - } - - // Clear existing markers - state.map.eachLayer(layer => { - if (layer instanceof L.Marker) state.map.removeLayer(layer); - }); - - // Add markers - state.people.forEach(p => { - if (p.location) { - // Simple geocoding simulation for demo - let lat = 0, lng = 0; - if (p.location.includes(',')) { - const parts = p.location.split(','); - lat = parseFloat(parts[0]); - lng = parseFloat(parts[1]); - } else if (p.location.toLowerCase().includes('grimstad')) { - lat = 58.34 + (Math.random() * 0.02); lng = 8.59 + (Math.random() * 0.02); - } else if (p.location.toLowerCase().includes('oslo')) { - lat = 59.91; lng = 10.75; - } - - if (lat && lng) { - L.marker([lat, lng]).addTo(state.map) - .bindPopup(`${p.name}
${p.location}`); - } - } - }); -} - -// --- TIMELINE RENDERER --- -function renderTimeline() { - const container = document.getElementById('timeline-list'); - if (!container) return; - - container.innerHTML = ''; - const events = []; - - state.people.forEach(p => { - if (p.birthDate) events.push({ date: p.birthDate, type: 'Født', person: p.name, desc: p.description }); - if (p.deathDate) events.push({ date: p.deathDate, type: 'Død', person: p.name }); - }); - - events.sort((a, b) => new Date(a.date) - new Date(b.date)); - - events.forEach(ev => { - const div = document.createElement('div'); - div.className = 'timeline-event'; - const dateStr = new Date(ev.date).toLocaleDateString('no-NO', { year: 'numeric', month: 'long', day: 'numeric' }); - - div.innerHTML = ` -
${dateStr}
-
- ${ev.person} ${ev.type.toLowerCase()} - ${ev.desc ? `

${ev.desc}

` : ''} -
- `; - container.appendChild(div); - }); -} - -// --- MODAL LOGIC --- -function setupModal() { - window.openModal = (action) => { - state.modal.action = action; - const overlay = document.getElementById('modal-overlay'); - if (overlay) overlay.classList.remove('hidden'); - - // Reset Form - document.querySelectorAll('.input-field').forEach(i => i.value = ''); - - // Set Title - const title = document.getElementById('modal-title'); - if (title) { - if (action === 'add-parent-m') title.textContent = "Legg til Far"; - else if (action === 'add-parent-f') title.textContent = "Legg til Mor"; - else if (action === 'add-child') title.textContent = "Legg til Barn"; - else if (action === 'add-partner') title.textContent = "Legg til Partner"; - else title.textContent = "Legg til Person"; - } - - // Default Mode - window.setModalMode('new'); - renderExistingList(); - }; - - window.setModalMode = (mode) => { - state.modal.mode = mode; - document.getElementById('tab-new').classList.toggle('active', mode === 'new'); - document.getElementById('tab-existing').classList.toggle('active', mode === 'existing'); - document.getElementById('form-new').classList.toggle('hidden', mode !== 'new'); - document.getElementById('form-existing').classList.toggle('hidden', mode !== 'existing'); - }; - - const closeBtn = document.getElementById('btn-close-modal'); - if (closeBtn) closeBtn.onclick = () => { - document.getElementById('modal-overlay').classList.add('hidden'); - }; - - const saveBtn = document.getElementById('btn-save'); - if (saveBtn) saveBtn.onclick = async () => { - const focusP = state.people.find(p => p.id === state.focusId); - let targetId = null; - - if (state.modal.mode === 'new') { - const newPerson = { - name: document.getElementById('inp-name').value, - birthDate: document.getElementById('inp-birth').value, - deathDate: document.getElementById('inp-death').value, - location: document.getElementById('inp-location').value, - description: document.getElementById('inp-desc').value, - gender: document.querySelector('input[name="gender"]:checked').value, - fatherId: null, motherId: null, partnerId: null - }; - - // Auto-link logic - if (state.modal.action === 'add-child' && focusP) { - if (focusP.gender === 'M') newPerson.fatherId = focusP.id; - else newPerson.motherId = focusP.id; - - if (focusP.partnerId) { - const part = state.people.find(p => p.id === focusP.partnerId); - if (part) { - if (part.gender === 'M') newPerson.fatherId = part.id; - else newPerson.motherId = part.id; - } - } - } - - const docRef = await addDoc(collection(db, 'familyMembers'), newPerson); - targetId = docRef.id; - - // If creating first person or using generic add - if (state.modal.action === 'create-first' || !state.focusId) { - state.focusId = targetId; - } - - } else { - targetId = document.getElementById('selected-existing-id').value; - } - - // Link Relations - if (focusP && targetId && state.modal.action !== 'create-first' && state.modal.action !== 'add-generic') { - const col = collection(db, 'familyMembers'); - const updates = []; - - if (state.modal.action === 'add-parent-m') updates.push(updateDoc(doc(col, state.focusId), { fatherId: targetId })); - else if (state.modal.action === 'add-parent-f') updates.push(updateDoc(doc(col, state.focusId), { motherId: targetId })); - else if (state.modal.action === 'add-partner') { - updates.push(updateDoc(doc(col, state.focusId), { partnerId: targetId })); - updates.push(updateDoc(doc(col, targetId), { partnerId: state.focusId })); - } else if (state.modal.action === 'add-child' && state.modal.mode === 'existing') { - const upd = {}; - if (focusP.gender === 'M') upd.fatherId = focusP.id; else upd.motherId = focusP.id; - updates.push(updateDoc(doc(col, targetId), upd)); - } - await Promise.all(updates); - } - - document.getElementById('modal-overlay').classList.add('hidden'); - }; -} - -function renderExistingList() { - const list = document.getElementById('existing-list'); - if (!list) return; - list.innerHTML = ''; - state.people.filter(p => p.id !== state.focusId).forEach(p => { - const div = document.createElement('div'); - div.style.padding = '0.5rem'; - div.style.borderBottom = '1px solid #f1f5f9'; - div.style.cursor = 'pointer'; - div.textContent = p.name; - div.onclick = () => { - document.getElementById('selected-existing-id').value = p.id; - div.style.background = '#f0fdf4'; - }; - list.appendChild(div); - }); -} - -// --- ZOOM --- -function setupZoom() { - const grid = document.getElementById('tree-grid'); - const display = document.getElementById('zoom-display'); - - const btnIn = document.getElementById('btn-zoom-in'); - if (btnIn) btnIn.onclick = () => { - state.scale += 0.1; - updateZoom(); - }; - - const btnOut = document.getElementById('btn-zoom-out'); - if (btnOut) btnOut.onclick = () => { - if (state.scale > 0.4) state.scale -= 0.1; - updateZoom(); - }; - - function updateZoom() { - if (grid) grid.style.transform = `scale(${state.scale})`; - if (display) display.textContent = Math.round(state.scale * 100) + '%'; - } -} - -// --- HELP --- -function setupHelp() { - const btn = document.getElementById('btn-help'); - const modal = document.getElementById('modal-help'); - const close = document.getElementById('btn-close-help'); - - if (btn && modal) { - btn.onclick = () => modal.classList.remove('hidden'); - } - if (close && modal) { - close.onclick = () => modal.classList.add('hidden'); - } -} - -// --- FAB --- -function setupFAB() { - const fab = document.getElementById('fab-add'); - if (fab) { - fab.onclick = () => window.openModal('add-generic'); - } -} diff --git a/app.html b/app.html deleted file mode 100644 index c0a0e67..0000000 --- a/app.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - Røsstad Familietre - - - - - - - - - - - - - -
- -
- - - - - - -
- -
- - - 100% - - -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/config.js b/config.js deleted file mode 100644 index bf7e3d6..0000000 --- a/config.js +++ /dev/null @@ -1,9 +0,0 @@ -// --- FIREBASE CONFIGURATION --- -export const firebaseConfig = { - apiKey: "AIzaSyD-E58BdRwFCHDmsgRG9-PEWzMaQoheJ-0", - authDomain: "roesstad-9fd48.firebaseapp.com", - projectId: "roesstad-9fd48", - storageBucket: "roesstad-9fd48.firebasestorage.app", - messagingSenderId: "704119736902", - appId: "1:704119736902:web:bc47f2b6a1383de0479847" -}; diff --git a/index.html b/index.html deleted file mode 100644 index dc1705d..0000000 --- a/index.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - Logg Inn - Røsstad Arkivet - - - - - - - - -
- - -

Røsstad Arkivet

- - -
- - - - -
- -
-
- - - - - \ No newline at end of file diff --git a/login.js b/login.js deleted file mode 100644 index 62e1da6..0000000 --- a/login.js +++ /dev/null @@ -1,92 +0,0 @@ -import { initializeApp } from "https://www.gstatic.com/firebasejs/10.14.1/firebase-app.js"; -import { getAuth, signInWithEmailAndPassword } from "https://www.gstatic.com/firebasejs/10.14.1/firebase-auth.js"; -import { firebaseConfig } from "./config.js"; - -// --- INIT --- -const app = initializeApp(firebaseConfig); -const auth = getAuth(app); - -// --- DOM ELEMENTS --- -const form = document.getElementById('login-form'); -const emailInput = document.getElementById('inp-email'); -const passwordInput = document.getElementById('inp-password'); -const loginBtn = document.getElementById('btn-login'); -const errorMsg = document.getElementById('error-msg'); - -// --- LOGIN LOGIC --- -if (form) { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const email = emailInput.value.trim(); - const password = passwordInput.value; - - if (!email || !password) { - showError("Vennligst fyll ut både e-post og passord."); - return; - } - - setLoading(true); - - try { - await signInWithEmailAndPassword(auth, email, password); - // Redirect on success - window.location.href = "app.html"; - } catch (error) { - console.error("Login error:", error); - handleError(error); - setLoading(false); - } - }); -} - -function setLoading(isLoading) { - if (isLoading) { - loginBtn.textContent = "Logger inn..."; - loginBtn.disabled = true; - errorMsg.textContent = ""; - } else { - loginBtn.textContent = "Logg Inn"; - loginBtn.disabled = false; - } -} - -function handleError(error) { - let message = "En ukjent feil oppstod."; - - switch (error.code) { - case 'auth/invalid-credential': - case 'auth/user-not-found': - case 'auth/wrong-password': - message = "Feil e-post eller passord."; - break; - case 'auth/too-many-requests': - message = "For mange forsøk. Vennligst vent litt og prøv igjen."; - break; - case 'auth/invalid-email': - message = "Ugyldig e-postadresse."; - break; - } - - showError(message); -} - -function showError(msg) { - errorMsg.textContent = msg; - // Shake animation effect - const container = document.querySelector('.login-container'); - container.style.animation = 'none'; - container.offsetHeight; /* trigger reflow */ - container.style.animation = 'shake 0.5s'; -} - -// Add shake keyframes dynamically -const style = document.createElement('style'); -style.innerHTML = ` -@keyframes shake { - 0%, 100% { transform: translateX(0); } - 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } - 20%, 40%, 60%, 80% { transform: translateX(5px); } -} -`; -document.head.appendChild(style); diff --git a/style.css b/style.css index 417af22..55e4f05 100644 --- a/style.css +++ b/style.css @@ -1,6 +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 --- */ @@ -27,492 +27,4 @@ --color-text-muted: #64748b; /* Slate 500 */ - max-width: 400px; - text-align: center; - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: var(--shadow-lg); -} - -.login-logo { - font-size: 4rem; - color: var(--color-primary-light); - margin-bottom: 1rem; -} - -.login-title { - font-size: 1.5rem; - font-weight: 700; - margin: 0 0 0.5rem 0; -} - -.login-subtitle { - font-size: 0.9rem; - opacity: 0.8; - margin-bottom: 2rem; - font-weight: 300; -} - -.input-field { - width: 100%; - padding: 1rem; - margin-bottom: 1rem; - border: none; - border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.95); - font-family: inherit; - font-size: 1rem; - transition: box-shadow 0.2s; -} - -.input-field:focus { - outline: none; - box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.5); -} - -.btn-primary { - width: 100%; - padding: 1rem; - border: none; - border-radius: var(--radius-md); - background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark)); - color: white; - font-weight: 600; - font-size: 1rem; - cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); -} - -.error-message { - color: #fda4af; - margin-top: 1rem; - font-size: 0.9rem; - min-height: 1.2em; -} - -/* --- MAIN APP LAYOUT --- */ -#app-root { - display: flex; - flex-direction: column; - height: 100vh; -} - -/* Header */ -.app-header { - background: var(--color-surface-glass); - backdrop-filter: blur(10px); - padding: 0.8rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - z-index: 100; -} - -.brand-logo { - display: flex; - align-items: center; - gap: 0.8rem; - font-size: 1.2rem; - font-weight: 700; - color: var(--color-primary-dark); -} - -.brand-logo i { - color: var(--color-primary); - font-size: 1.5rem; -} - -/* Navigation Tabs */ -.nav-tabs { - display: flex; - gap: 0.5rem; - background: rgba(0, 0, 0, 0.05); - padding: 0.3rem; - border-radius: var(--radius-md); -} - -.nav-tab { - padding: 0.5rem 1.2rem; - border: none; - background: transparent; - border-radius: var(--radius-sm); - color: var(--color-text-muted); - font-weight: 600; - cursor: pointer; - transition: all 0.2s; -} - -.nav-tab.active { - background: white; - color: var(--color-primary); - box-shadow: var(--shadow-sm); -} - -.nav-tab:hover:not(.active) { - color: var(--color-primary); - background: rgba(255, 255, 255, 0.5); -} - -/* Controls */ -.header-controls { - display: flex; - align-items: center; - gap: 1rem; -} - -.icon-btn { - background: none; - border: none; - font-size: 1.5rem; - color: var(--color-text-muted); - cursor: pointer; - transition: color 0.2s; - display: flex; -} - -.icon-btn:hover { - color: var(--color-primary); -} - -.btn-logout { - font-size: 0.9rem; - padding: 0.4rem 1rem; - border: 1px solid var(--color-primary); - color: var(--color-primary); - background: transparent; - border-radius: var(--radius-sm); - cursor: pointer; - font-weight: 600; -} - -.btn-logout:hover { - background: var(--color-primary); - color: white; -} - -/* --- VIEW SECTIONS --- */ -.view-section { - flex: 1; - overflow: hidden; - position: relative; -} - -/* Tree View */ -#tree-container { - width: 100%; - height: 100%; - overflow: auto; - display: flex; - justify-content: center; - align-items: center; - padding: 4rem; - cursor: grab; -} - -#tree-container:active { - cursor: grabbing; -} - -.tree-grid { - display: flex; - flex-direction: column; - align-items: center; - gap: 5rem; - transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); -} - -.tree-row { - display: flex; - justify-content: center; - gap: 3rem; -} - -.tree-row.children { - gap: 2rem; - flex-wrap: wrap; -} - -/* Person Card */ -.card { - width: 200px; - min-height: 130px; - background: var(--color-surface-glass); - border: 1px solid white; - border-radius: var(--radius-md); - padding: 1.2rem; - display: flex; - flex-direction: column; - align-items: center; - box-shadow: var(--shadow-md); - cursor: pointer; - transition: all 0.3s; - position: relative; -} - -.card:hover { - transform: translateY(-5px); - box-shadow: var(--shadow-lg); - border-color: var(--color-primary-light); -} - -.card.focus { - transform: scale(1.15); - z-index: 10; - border: 2px solid var(--color-primary); - background: white; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); -} - -.card-avatar { - font-size: 2rem; - padding: 0.5rem; - border-radius: 50%; - margin-bottom: 0.5rem; -} - -.card.male .card-avatar { - color: var(--color-male); - background: #e0f2fe; -} - -.card.female .card-avatar { - color: var(--color-female); - background: #ffe4e6; -} - -.card-name { - font-size: 1rem; - font-weight: 700; - margin: 0.2rem 0; - text-align: center; -} - -.card-dates { - font-size: 0.8rem; - color: var(--color-text-muted); - font-family: monospace; -} - -.card-location { - font-size: 0.7rem; - margin-top: 0.5rem; - padding: 2px 8px; - background: rgba(45, 212, 191, 0.15); - color: var(--color-primary-dark); - border-radius: 10px; - display: flex; - align-items: center; - gap: 4px; -} - -/* Add Buttons (Mini) */ -.mini-actions { - position: absolute; - inset: 0; - pointer-events: none; -} - -.btn-mini { - pointer-events: auto; - position: absolute; - width: 32px; - height: 32px; - border-radius: 50%; - background: var(--color-primary); - color: white; - border: none; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - box-shadow: var(--shadow-md); - transition: transform 0.2s; -} - -.btn-mini:hover { - transform: scale(1.2); - background: var(--color-primary-dark); -} - -.btn-add-child { - bottom: -16px; - left: 50%; - transform: translateX(-50%); -} - -.btn-add-partner { - top: 50%; - right: -16px; - transform: translateY(-50%); -} - -/* Empty Slot */ -.slot { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; -} - -.slot-label { - font-size: 0.7rem; - font-weight: 700; - text-transform: uppercase; - color: var(--color-text-muted); - opacity: 0.7; -} - -.btn-slot { - width: 50px; - height: 50px; - border-radius: 50%; - border: 2px dashed #cbd5e1; - background: rgba(255, 255, 255, 0.5); - color: #94a3b8; - display: flex; - justify-content: center; - align-items: center; - font-size: 1.5rem; - cursor: pointer; - transition: all 0.2s; -} - -.btn-slot:hover { - border-color: var(--color-primary); - overflow-y: auto; - height: 100%; -} - -.timeline-event { - position: relative; - padding-left: 2rem; - margin-bottom: 2rem; - border-left: 2px solid var(--color-primary-light); -} - -.timeline-event::before { - content: ''; - position: absolute; - left: -6px; - top: 0; - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--color-primary); -} - -.event-date { - font-weight: 700; - color: var(--color-primary); - font-size: 0.9rem; -} - -.event-card { - background: white; - padding: 1rem; - border-radius: var(--radius-md); - box-shadow: var(--shadow-sm); - margin-top: 0.5rem; -} - -/* --- MODAL --- */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(15, 47, 36, 0.6); - backdrop-filter: blur(4px); - z-index: 1000; - display: flex; - justify-content: center; - align-items: center; - opacity: 0; - animation: fadeIn 0.2s forwards; -} - -@keyframes fadeIn { - to { - opacity: 1; - } -} - -.modal-content { - background: white; - width: 90%; - max-width: 500px; - border-radius: var(--radius-lg); - padding: 2rem; - box-shadow: var(--shadow-lg); - transform: scale(0.95); - animation: popIn 0.3s forwards; -} - -@keyframes popIn { - to { - transform: scale(1); - } -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - border-bottom: 1px solid #f1f5f9; - padding-bottom: 1rem; -} - -.modal-title { - font-size: 1.4rem; - font-weight: 700; - color: var(--color-primary-dark); - margin: 0; -} - -.form-group { - margin-bottom: 1rem; -} - - -.form-label { - display: block; - font-size: 0.85rem; - font-weight: 700; - margin-bottom: 0.4rem; -} - -.form-row { - 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; - 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