diff --git a/kjvstudy_org/server.py b/kjvstudy_org/server.py
index 37e9467..9a8cea3 100644
--- a/kjvstudy_org/server.py
+++ b/kjvstudy_org/server.py
@@ -2488,6 +2488,8 @@ def strongs_greek_index(request: Request, page: int = 1):
@app.get("/strongs/{strongs_number}", response_class=HTMLResponse)
def strongs_entry(request: Request, strongs_number: str):
"""View a single Strong's concordance entry."""
+ import re
+
entry = format_strongs_entry(strongs_number)
if not entry:
@@ -2517,6 +2519,20 @@ def strongs_entry(request: Request, strongs_number: str):
else:
occ["verse_text"] = ""
+ # Extract and fetch related Strong's entries from derivation
+ related_entries = []
+ if entry.get("derivation"):
+ # Find all Strong's references like H1234 or G5678
+ strongs_refs = re.findall(r'([HG])(\d+)', entry["derivation"])
+ seen = set()
+ for prefix, num in strongs_refs:
+ ref = f"{prefix}{num}"
+ if ref.upper() != strongs_number.upper() and ref not in seen:
+ seen.add(ref)
+ related = format_strongs_entry(ref)
+ if related:
+ related_entries.append(related)
+
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
@@ -2532,6 +2548,7 @@ def strongs_entry(request: Request, strongs_number: str):
"books": books,
"breadcrumbs": breadcrumbs,
"verse_occurrences": verse_occurrences,
- "total_occurrences": total_occurrences
+ "total_occurrences": total_occurrences,
+ "related_entries": related_entries
}
)
diff --git a/kjvstudy_org/templates/family_tree.html b/kjvstudy_org/templates/family_tree.html
index c183f91..4620139 100644
--- a/kjvstudy_org/templates/family_tree.html
+++ b/kjvstudy_org/templates/family_tree.html
@@ -56,6 +56,7 @@
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
+ box-sizing: border-box;
}
.search-form input:focus {
diff --git a/kjvstudy_org/templates/family_tree_interactive.html b/kjvstudy_org/templates/family_tree_interactive.html
index f753cbc..b8ffed0 100644
--- a/kjvstudy_org/templates/family_tree_interactive.html
+++ b/kjvstudy_org/templates/family_tree_interactive.html
@@ -203,6 +203,59 @@
transform: translateX(0);
}
+/* Hamburger toggle button */
+.sidebar-toggle {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ width: 28px;
+ height: 48px;
+ background: white;
+ border: 1px solid #ddd;
+ border-right: none;
+ border-radius: 6px 0 0 6px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ box-shadow: -2px 0 6px rgba(0,0,0,0.1);
+ z-index: 101;
+ transition: right 0.3s ease, background 0.15s ease;
+}
+
+.sidebar-toggle:hover {
+ background: #f5f5f5;
+}
+
+.sidebar-toggle .hamburger-line {
+ width: 16px;
+ height: 2px;
+ background: #666;
+ border-radius: 1px;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+}
+
+/* When sidebar is open, move toggle and show X */
+.info-sidebar.open + .sidebar-toggle,
+.sidebar-toggle.active {
+ right: 280px;
+}
+
+.sidebar-toggle.active .hamburger-line:nth-child(1) {
+ transform: rotate(45deg) translate(4px, 4px);
+}
+
+.sidebar-toggle.active .hamburger-line:nth-child(2) {
+ opacity: 0;
+}
+
+.sidebar-toggle.active .hamburger-line:nth-child(3) {
+ transform: rotate(-45deg) translate(4px, -4px);
+}
+
.info-sidebar-header {
display: flex;
align-items: center;
@@ -252,13 +305,28 @@
color: #3b6ea5;
}
+.info-field-value a.tree-nav-link {
+ color: #4a7c59;
+ cursor: pointer;
+ text-decoration: underline;
+ text-decoration-style: dotted;
+}
+
+.info-field-value a.tree-nav-link:hover {
+ color: #3d6a4b;
+ text-decoration-style: solid;
+}
+
.info-actions {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #eee;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
}
-.info-actions a {
+.info-actions a.info-btn-primary {
display: block;
padding: 0.6rem 1rem;
background: #4a7c59;
@@ -270,10 +338,28 @@
transition: background 0.15s ease;
}
-.info-actions a:hover {
+.info-actions a.info-btn-primary:hover {
background: #3d6a4b;
}
+.info-actions a.info-btn-secondary {
+ display: block;
+ padding: 0.5rem 1rem;
+ background: transparent;
+ color: #3b6ea5;
+ text-align: center;
+ border: 1px solid #3b6ea5;
+ border-radius: 4px;
+ text-decoration: none;
+ font-size: 0.9rem;
+ transition: all 0.15s ease;
+}
+
+.info-actions a.info-btn-secondary:hover {
+ background: #3b6ea5;
+ color: white;
+}
+
/* Navigation hint */
.tree-hint {
position: absolute;
@@ -525,6 +611,19 @@
color: #e0e0e0;
}
+[data-theme="dark"] .sidebar-toggle {
+ background: #1a1a1a;
+ border-color: #444;
+}
+
+[data-theme="dark"] .sidebar-toggle:hover {
+ background: #2a2a2a;
+}
+
+[data-theme="dark"] .sidebar-toggle .hamburger-line {
+ background: #aaa;
+}
+
[data-theme="dark"] .info-sidebar {
background: #1a1a1a;
border-left-color: #444;
@@ -551,10 +650,28 @@
color: #e0e0e0;
}
+[data-theme="dark"] .info-field-value a.tree-nav-link {
+ color: #6b9b7a;
+}
+
+[data-theme="dark"] .info-field-value a.tree-nav-link:hover {
+ color: #8bc49a;
+}
+
[data-theme="dark"] .info-actions {
border-top-color: #333;
}
+[data-theme="dark"] .info-actions a.info-btn-secondary {
+ color: #6b9b7a;
+ border-color: #6b9b7a;
+}
+
+[data-theme="dark"] .info-actions a.info-btn-secondary:hover {
+ background: #6b9b7a;
+ color: #1a1a1a;
+}
+
[data-theme="dark"] .tree-hint {
background: rgba(30, 30, 30, 0.95);
border-color: #444;
@@ -698,20 +815,33 @@
+
+
- Click to expand/collapse • Double-click to focus • Scroll to zoom • Drag to pan
+ Click i for info • Click + to expand • Double-click to focus • Scroll to zoom • Drag to pan
@@ -1016,6 +1146,7 @@ function createPersonCard(d, showSpouse = true) {
const hasExpandableChildren = data.hasChildren && !d.children && !d._children;
const isCollapsed = hasHiddenChildren || hasExpandableChildren;
const hasKekule = data.kekule_number !== null && data.kekule_number !== undefined;
+ const isAncestor = data.isAncestor === true; // Check if this is an ancestor node
let dates = '';
if (data.birth_year && data.birth_year !== 'Unknown') {
@@ -1026,13 +1157,14 @@ function createPersonCard(d, showSpouse = true) {
}
const group = d3.create('svg:g')
- .attr('class', 'person-node')
+ .attr('class', 'person-node' + (isAncestor ? ' ancestor-node' : ''))
.attr('data-id', d.data.id)
- .style('cursor', 'pointer');
+ .style('cursor', 'pointer')
+ .style('opacity', isAncestor ? 0.6 : 1); // Make ancestors semi-transparent
- // Card dimensions
- const cardWidth = 150;
- const cardHeight = dates ? 58 : 42;
+ // Card dimensions (narrower cards)
+ const cardWidth = 120;
+ const cardHeight = dates ? 54 : 40;
// Determine colors based on gender and Kekulé status
let fillColor, strokeColor;
@@ -1079,10 +1211,10 @@ function createPersonCard(d, showSpouse = true) {
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-family', '"ETBembo", Palatino, "Book Antiqua", serif')
- .attr('font-size', '13px')
+ .attr('font-size', '12px')
.attr('font-weight', '600')
.attr('fill', '#222')
- .text(data.name.length > 18 ? data.name.substring(0, 16) + '…' : data.name);
+ .text(data.name.length > 14 ? data.name.substring(0, 12) + '…' : data.name);
// Dates text
if (dates) {
@@ -1092,7 +1224,7 @@ function createPersonCard(d, showSpouse = true) {
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-family', '"ETBembo", Palatino, "Book Antiqua", serif')
- .attr('font-size', '11px')
+ .attr('font-size', '10px')
.attr('fill', '#666')
.text(dates);
}
@@ -1156,7 +1288,6 @@ function createPersonCard(d, showSpouse = true) {
navigationHistory.push(currentRootId);
}
currentRootId = spouseId;
- transform = d3.zoomIdentity;
renderTreeFromId(spouseId);
updateBreadcrumb(spouseId);
}
@@ -1179,9 +1310,9 @@ function createPersonCard(d, showSpouse = true) {
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-family', '"ETBembo", Palatino, "Book Antiqua", serif')
- .attr('font-size', '11px')
+ .attr('font-size', '10px')
.attr('fill', '#555')
- .text(data.spouse.length > 16 ? data.spouse.substring(0, 14) + '…' : data.spouse);
+ .text(data.spouse.length > 12 ? data.spouse.substring(0, 10) + '…' : data.spouse);
// Ring icon to indicate marriage
spouseGroup.append('text')
@@ -1193,8 +1324,8 @@ function createPersonCard(d, showSpouse = true) {
.text('💍');
}
- // Expand/collapse indicator (shows + if collapsed with children, - if expanded)
- if (isCollapsed || (d.children && d.children.length > 0)) {
+ // Expand indicator (only shows + when there are hidden/expandable children)
+ if (isCollapsed) {
const indicatorX = cardWidth / 2 + 8;
const indicatorY = 0;
@@ -1203,7 +1334,7 @@ function createPersonCard(d, showSpouse = true) {
.attr('cx', indicatorX)
.attr('cy', indicatorY)
.attr('r', 10)
- .attr('fill', isCollapsed ? '#4a7c59' : '#888')
+ .attr('fill', '#4a7c59')
.attr('stroke', '#fff')
.attr('stroke-width', 1);
@@ -1216,9 +1347,35 @@ function createPersonCard(d, showSpouse = true) {
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr('fill', 'white')
- .text(isCollapsed ? '+' : '−');
+ .text('+');
}
+ // Info button (always shown, on the left side)
+ const infoX = -cardWidth / 2 - 8;
+ const infoY = 0;
+
+ group.append('circle')
+ .attr('class', 'info-btn')
+ .attr('cx', infoX)
+ .attr('cy', infoY)
+ .attr('r', 10)
+ .attr('fill', '#3b6ea5')
+ .attr('stroke', '#fff')
+ .attr('stroke-width', 1)
+ .style('cursor', 'pointer');
+
+ group.append('text')
+ .attr('class', 'info-icon')
+ .attr('x', infoX)
+ .attr('y', infoY)
+ .attr('text-anchor', 'middle')
+ .attr('dominant-baseline', 'middle')
+ .attr('font-size', '12px')
+ .attr('font-weight', 'bold')
+ .attr('fill', 'white')
+ .attr('pointer-events', 'none')
+ .text('i');
+
return group.node();
}
@@ -1314,9 +1471,9 @@ function renderTree() {
// Create hierarchy
const root = d3.hierarchy(treeData);
- // Calculate tree dimensions
- const nodeWidth = 180;
- const nodeHeight = 80;
+ // Calculate tree dimensions (narrower spacing for narrower cards)
+ const nodeWidth = 145;
+ const nodeHeight = 75;
const isHorizontal = currentOrientation === 'left-right';
// Tree layout
@@ -1371,26 +1528,34 @@ function renderTree() {
this.appendChild(card);
});
- // Single click: expand/collapse children OR show info if leaf node
+ // Single click handler
nodes.on('click', (event, d) => {
event.stopPropagation();
- // Check if clicking on the expand indicator
const target = event.target;
- const isExpandClick = target.classList.contains('expand-indicator') ||
- target.classList.contains('expand-icon') ||
- target.closest('.expand-indicator');
- // If has children (visible or hidden) or can be expanded, toggle on click
- const canToggle = d.children || d._children ||
- (d.data.data && d.data.data.hasChildren);
+ // Check if clicking on the info button
+ const isInfoClick = target.classList.contains('info-btn') ||
+ target.classList.contains('info-icon');
- if (canToggle) {
- toggleNode(d);
- } else {
- // Leaf node - show info
+ if (isInfoClick) {
showPersonInfo(d.data);
+ return;
}
+
+ // Check if clicking on the expand indicator
+ const isExpandClick = target.classList.contains('expand-indicator') ||
+ target.classList.contains('expand-icon');
+
+ // Check if node can be expanded/collapsed
+ const hasHiddenChildren = d._children && d._children.length > 0;
+ const hasExpandableChildren = d.data.data && d.data.data.hasChildren && !d.children && !d._children;
+ const canExpand = hasHiddenChildren || hasExpandableChildren;
+
+ if (isExpandClick && canExpand) {
+ toggleNode(d);
+ }
+ // Clicking anywhere else on the card does nothing (use info button or double-click)
});
// Double click: re-center tree on this person
@@ -1429,10 +1594,7 @@ function renderTree() {
rootSelect.value = 'custom';
}
- // Reset zoom transform for fresh view
- transform = d3.zoomIdentity;
-
- // Re-render with new root
+ // Re-render with new root (zoom level preserved, view tweens to center)
renderTreeFromId(personId);
updateBreadcrumb(personId);
});
@@ -1458,7 +1620,10 @@ function renderTree() {
}
// Re-render tree from a specific person ID (bidirectional - shows parents and children)
-function renderTreeFromId(personId) {
+function renderTreeFromId(personId, animate = true) {
+ // Store current zoom scale to preserve it
+ const previousScale = transform.k || 1;
+
container.innerHTML = '';
const treeData = buildBidirectionalTreeData(personId, currentDepth, 2);
@@ -1488,8 +1653,8 @@ function renderTreeFromId(personId) {
svg.call(zoom);
- const nodeWidth = 180;
- const nodeHeight = 100;
+ const nodeWidth = 145;
+ const nodeHeight = 90;
const isHorizontal = currentOrientation === 'left-right';
// We'll render two trees: ancestors above and descendants below
@@ -1614,10 +1779,20 @@ function renderTreeFromId(personId) {
this.appendChild(card);
});
- // Single click: show info
+ // Single click: handle info button
nodes.on('click', (event, d) => {
event.stopPropagation();
- showPersonInfo(d.data);
+
+ const target = event.target;
+
+ // Check if clicking on the info button
+ const isInfoClick = target.classList.contains('info-btn') ||
+ target.classList.contains('info-icon');
+
+ if (isInfoClick) {
+ showPersonInfo(d.data);
+ }
+ // Clicking elsewhere does nothing (use info button or double-click)
});
// Double click: re-center
@@ -1632,7 +1807,6 @@ function renderTreeFromId(personId) {
}
currentRootId = personId;
- transform = d3.zoomIdentity;
renderTreeFromId(personId);
updateBreadcrumb(personId);
});
@@ -1653,7 +1827,8 @@ function renderTreeFromId(personId) {
window.treeSvg = svg;
window.treeG = g;
- fitToView();
+ // Center on root person with smooth animation, preserving zoom level
+ centerOnRootPerson(previousScale, animate);
}
// Update tree after expand/collapse with animation
@@ -1662,8 +1837,8 @@ function updateTree(source) {
const g = window.treeG;
const isHorizontal = currentOrientation === 'left-right';
- const nodeWidth = 180;
- const nodeHeight = 80;
+ const nodeWidth = 145;
+ const nodeHeight = 75;
// Re-compute tree layout
const treeLayout = d3.tree()
@@ -1735,12 +1910,29 @@ function updateTree(source) {
nodesEnter
.on('click', (event, d) => {
event.stopPropagation();
- const canToggle = d.children || d._children ||
- (d.data.data && d.data.data.hasChildren);
- if (canToggle) {
- toggleNode(d);
- } else {
+
+ const target = event.target;
+
+ // Check if clicking on the info button
+ const isInfoClick = target.classList.contains('info-btn') ||
+ target.classList.contains('info-icon');
+
+ if (isInfoClick) {
showPersonInfo(d.data);
+ return;
+ }
+
+ // Check if clicking on the expand indicator
+ const isExpandClick = target.classList.contains('expand-indicator') ||
+ target.classList.contains('expand-icon');
+
+ // Check if node can be expanded
+ const hasHiddenChildren = d._children && d._children.length > 0;
+ const hasExpandableChildren = d.data.data && d.data.data.hasChildren && !d.children && !d._children;
+ const canExpand = hasHiddenChildren || hasExpandableChildren;
+
+ if (isExpandClick && canExpand) {
+ toggleNode(d);
}
})
.on('dblclick', (event, d) => {
@@ -1754,7 +1946,6 @@ function updateTree(source) {
}
currentRootId = personId;
- transform = d3.zoomIdentity;
renderTreeFromId(personId);
updateBreadcrumb(personId);
})
@@ -1813,6 +2004,39 @@ function updateTree(source) {
});
}
+// Center view on the root person (selected person) with smooth animation
+function centerOnRootPerson(targetScale = 1, animate = true) {
+ const svg = window.treeSvg;
+ const zoom = window.treeZoom;
+ if (!svg || !zoom || !treeRoot) return;
+
+ const width = container.clientWidth;
+ const height = container.clientHeight;
+ const isHorizontal = currentOrientation === 'left-right';
+
+ // Get root person's position (treeRoot is always the selected person)
+ const rootX = isHorizontal ? treeRoot.y : treeRoot.x;
+ const rootY = isHorizontal ? treeRoot.x : treeRoot.y;
+
+ // Clamp scale to reasonable bounds
+ const scale = Math.max(0.5, Math.min(targetScale, 2.0));
+
+ // Calculate transform to center root person
+ const tx = width / 2 - rootX * scale;
+ const ty = height / 2 - rootY * scale;
+
+ const newTransform = d3.zoomIdentity.translate(tx, ty).scale(scale);
+
+ if (animate) {
+ svg.transition()
+ .duration(600)
+ .ease(d3.easeCubicInOut)
+ .call(zoom.transform, newTransform);
+ } else {
+ svg.call(zoom.transform, newTransform);
+ }
+}
+
// Fit tree to view
function fitToView() {
const svg = window.treeSvg;
@@ -1849,6 +2073,17 @@ function showPersonInfo(person) {
document.getElementById('sidebar-name').textContent = data.name;
document.getElementById('view-profile-link').href = `/family-tree/person/${person.id}`;
+ document.getElementById('view-ancestors-link').href = `/family-tree/person/${person.id}/ancestors`;
+ document.getElementById('view-descendants-link').href = `/family-tree/person/${person.id}/descendants`;
+
+ // Title/Occupation
+ const titleField = document.getElementById('field-title');
+ if (fullPerson && fullPerson.title && fullPerson.title !== 'Biblical Figure') {
+ titleField.innerHTML = `Title
${fullPerson.title}
`;
+ titleField.style.display = 'block';
+ } else {
+ titleField.style.display = 'none';
+ }
// Dates
const datesField = document.getElementById('field-dates');
@@ -1883,11 +2118,20 @@ function showPersonInfo(person) {
genField.style.display = 'none';
}
+ // Kekulé number (bloodline to Christ)
+ const kekuleField = document.getElementById('field-kekule');
+ if (data.kekule_number !== null && data.kekule_number !== undefined) {
+ kekuleField.innerHTML = `Kekulé Number
#${data.kekule_number} (ancestor of Christ)
`;
+ kekuleField.style.display = 'block';
+ } else {
+ kekuleField.style.display = 'none';
+ }
+
// Spouse
const spouseField = document.getElementById('field-spouse');
if (data.spouse) {
const spouseId = findPersonId(data.spouse);
- const spouseLink = spouseId ? `${data.spouse}` : data.spouse;
+ const spouseLink = spouseId ? `${data.spouse}` : data.spouse;
spouseField.innerHTML = `Spouse
${spouseLink}
`;
spouseField.style.display = 'block';
} else {
@@ -1899,7 +2143,7 @@ function showPersonInfo(person) {
if (fullPerson && fullPerson.parents && fullPerson.parents.length > 0) {
const parentLinks = fullPerson.parents.map(pid => {
const parent = familyTreeData[pid];
- return parent ? `${parent.name}` : pid;
+ return parent ? `${parent.name}` : pid;
}).join(', ');
parentsField.innerHTML = `Parents
${parentLinks}
`;
parentsField.style.display = 'block';
@@ -1907,24 +2151,105 @@ function showPersonInfo(person) {
parentsField.style.display = 'none';
}
+ // Siblings
+ const siblingsField = document.getElementById('field-siblings');
+ if (fullPerson && fullPerson.siblings && fullPerson.siblings.length > 0) {
+ const siblingLinks = fullPerson.siblings.slice(0, 5).map(sid => {
+ const sibling = familyTreeData[sid];
+ return sibling ? `${sibling.name}` : sid;
+ }).join(', ');
+ const moreText = fullPerson.siblings.length > 5 ? ` +${fullPerson.siblings.length - 5} more` : '';
+ siblingsField.innerHTML = `Siblings
${siblingLinks}${moreText}
`;
+ siblingsField.style.display = 'block';
+ } else {
+ siblingsField.style.display = 'none';
+ }
+
// Children
const childrenField = document.getElementById('field-children');
if (fullPerson && fullPerson.children && fullPerson.children.length > 0) {
- const childCount = fullPerson.children.length;
- childrenField.innerHTML = `Children
${childCount} child${childCount !== 1 ? 'ren' : ''}
`;
+ const childLinks = fullPerson.children.slice(0, 5).map(cid => {
+ const child = familyTreeData[cid];
+ return child ? `${child.name}` : cid;
+ }).join(', ');
+ const moreText = fullPerson.children.length > 5 ? ` +${fullPerson.children.length - 5} more` : '';
+ childrenField.innerHTML = `Children
${childLinks}${moreText}
`;
childrenField.style.display = 'block';
} else {
childrenField.style.display = 'none';
}
+ // Biblical verse reference
+ const verseField = document.getElementById('field-verse');
+ if (fullPerson && fullPerson.verses && fullPerson.verses.length > 0) {
+ const verse = fullPerson.verses[0];
+ const verseHtml = `${verse.reference}
"${verse.text.substring(0, 100)}${verse.text.length > 100 ? '...' : ''}"`;
+ verseField.innerHTML = `Scripture
${verseHtml}
`;
+ verseField.style.display = 'block';
+ } else {
+ verseField.style.display = 'none';
+ }
+
sidebar.classList.add('open');
+ sidebarToggle.classList.add('active');
+
+ // Add click handlers for tree navigation links in sidebar
+ sidebar.querySelectorAll('.tree-nav-link').forEach(link => {
+ link.addEventListener('click', (event) => {
+ event.preventDefault();
+ const personId = link.dataset.personId;
+ if (personId) {
+ // Push current root to history before navigating
+ if (currentRootId && currentRootId !== personId) {
+ navigationHistory.push(currentRootId);
+ }
+ currentRootId = personId;
+
+ // Close sidebar if not pinned
+ if (!sidebarPinned) {
+ sidebar.classList.remove('open');
+ sidebarToggle.classList.remove('active');
+ }
+
+ // Render tree from this person (zoom preserved, view tweens)
+ renderTreeFromId(personId);
+ updateBreadcrumb(personId);
+ }
+ });
+ });
}
-// Close sidebar
-document.getElementById('sidebar-close').addEventListener('click', () => {
- sidebar.classList.remove('open');
+// Sidebar toggle functionality with persistence
+const sidebarToggle = document.getElementById('sidebar-toggle');
+let sidebarPinned = localStorage.getItem('familyTreeSidebarPinned') === 'true';
+
+function updateSidebarState() {
+ if (sidebarPinned) {
+ sidebar.classList.add('open');
+ sidebarToggle.classList.add('active');
+ } else {
+ sidebar.classList.remove('open');
+ sidebarToggle.classList.remove('active');
+ }
+}
+
+// Toggle sidebar when hamburger is clicked
+sidebarToggle.addEventListener('click', () => {
+ sidebarPinned = !sidebarPinned;
+ localStorage.setItem('familyTreeSidebarPinned', sidebarPinned);
+ updateSidebarState();
});
+// Close sidebar (X button inside sidebar)
+document.getElementById('sidebar-close').addEventListener('click', () => {
+ sidebarPinned = false;
+ localStorage.setItem('familyTreeSidebarPinned', 'false');
+ updateSidebarState();
+});
+
+// Initialize sidebar state from localStorage
+updateSidebarState();
+
// Controls
document.getElementById('root-select').addEventListener('change', (e) => {
currentRoot = e.target.value;
@@ -2044,8 +2369,7 @@ function selectPerson(personId) {
customOpt.textContent = personName;
rootSelect.value = 'custom';
- // Reset zoom and render
- transform = d3.zoomIdentity;
+ // Render with smooth transition (zoom preserved)
renderTreeFromId(personId);
updateBreadcrumb(personId);
}
diff --git a/kjvstudy_org/templates/strongs_entry.html b/kjvstudy_org/templates/strongs_entry.html
index cf14733..cd8b1bf 100644
--- a/kjvstudy_org/templates/strongs_entry.html
+++ b/kjvstudy_org/templates/strongs_entry.html
@@ -132,12 +132,77 @@
border-left: 4px solid #4169E1;
}
-.info-card.derivation p {
+.info-card.derivation > p {
font-size: 0.95rem;
color: var(--text-secondary);
font-style: italic;
}
+/* Related entries embedded in derivation */
+.related-entries {
+ margin-top: 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.related-entry-card {
+ padding: 0.75rem 1rem;
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ border-left: 3px solid #6495ED;
+}
+
+.related-entry-card:hover {
+ border-color: #4169E1;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+}
+
+.related-entry-header {
+ display: flex;
+ align-items: baseline;
+ gap: 0.75rem;
+ margin-bottom: 0.35rem;
+}
+
+.related-entry-header a {
+ font-weight: 600;
+ font-size: 1rem;
+ color: #4169E1;
+ text-decoration: none;
+}
+
+.related-entry-header a:hover {
+ text-decoration: underline;
+}
+
+.related-entry-lemma {
+ font-size: 1.1rem;
+}
+
+.related-entry-translit {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ font-style: italic;
+}
+
+.related-entry-def {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ margin: 0;
+ line-height: 1.5;
+}
+
+[data-theme="dark"] .related-entry-card {
+ background: #1a1a1a;
+ border-color: #333;
+}
+
+[data-theme="dark"] .related-entry-header a {
+ color: #6495ED;
+}
+
.strongs-ref {
color: var(--link-color);
text-decoration: none;
@@ -348,6 +413,20 @@
Derivation
{{ entry.derivation | linkify_strongs | safe }}
+ {% if related_entries %}
+
+ {% endif %}
{% endif %}