Add PWA manifest and improve Strong's keyboard navigation

- Add manifest.json for PWA support (stops 404 requests)
- Add [ and ] keys to navigate between Strong's numbers
- Add info-card selection styles for keyboard nav
- Update accessibility page with Strong's shortcuts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-03 13:52:16 -05:00
parent 45cca004f3
commit 4dd606e4a0
3 changed files with 95 additions and 22 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"name": "KJV Study",
"short_name": "KJV Study",
"description": "Study the King James Bible with AI-powered commentary, Strong's Concordance, and interlinear analysis",
"start_url": "/",
"display": "standalone",
"background_color": "#fffff8",
"theme_color": "#4a7c59"
}
+32
View File
@@ -289,6 +289,38 @@
</tbody>
</table>
<h3>Strong's Concordance Pages</h3>
<table class="keyboard-table">
<thead>
<tr>
<th>Key</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>[</kbd></td>
<td>Previous Strong's number</td>
</tr>
<tr>
<td><kbd>]</kbd></td>
<td>Next Strong's number</td>
</tr>
<tr>
<td><kbd>j</kbd> or <kbd></kbd></td>
<td>Select next card (info, related, occurrence)</td>
</tr>
<tr>
<td><kbd>k</kbd> or <kbd></kbd></td>
<td>Select previous card</td>
</tr>
<tr>
<td><kbd>Enter</kbd></td>
<td>Follow link on selected card</td>
</tr>
</tbody>
</table>
<p>Navigation keys are inspired by Vim conventions (<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>) for users familiar with that paradigm, while also supporting standard arrow keys for everyone else.</p>
<h3>A Note on Keyboard Navigation</h3>
+54 -22
View File
@@ -121,6 +121,13 @@
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 1.5rem;
transition: all 0.15s;
}
.info-card.selected {
border-color: var(--link-color);
box-shadow: 0 0 0 2px rgba(74, 124, 89, 0.3);
background: #f8fff8;
}
.info-card h2 {
@@ -230,6 +237,12 @@
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.related-entry-card.selected {
border-color: var(--link-color);
box-shadow: 0 0 0 2px rgba(74, 124, 89, 0.3);
background: #f8fff8;
}
.related-entry-header {
display: flex;
align-items: baseline;
@@ -270,6 +283,11 @@
border-color: #333;
}
[data-theme="dark"] .related-entry-card.selected {
background: #1a2a1a;
box-shadow: 0 0 0 2px rgba(107, 155, 122, 0.3);
}
[data-theme="dark"] .related-entry-header a {
color: #6495ED;
}
@@ -480,6 +498,11 @@
border-color: #333;
}
[data-theme="dark"] .info-card.selected {
background: #1a2a1a;
box-shadow: 0 0 0 2px rgba(107, 155, 122, 0.3);
}
[data-theme="dark"] .kjv-tag {
background: #2a2a2a;
border-color: #444;
@@ -551,9 +574,9 @@
{{ entry.strongs }}
<span class="lang-badge {% if entry.language == 'Hebrew' %}hebrew{% else %}greek{% endif %}">{{ entry.language }}</span>
</div>
<p class="original-word {% if entry.language == 'Hebrew' %}hebrew{% else %}greek{% endif %}" lang="{% if entry.language == 'Hebrew' %}he{% else %}el{% endif %}">{{ entry.word }}</p>
<p class="transliteration"><em>{{ entry.transliteration }}</em>{% if entry.pronunciation %} ({{ entry.pronunciation }}){% endif %}</p>
<p class="quick-def"><strong>Definition:</strong> {{ entry.definition }}</p>
<div class="original-word {% if entry.language == 'Hebrew' %}hebrew{% else %}greek{% endif %}" lang="{% if entry.language == 'Hebrew' %}he{% else %}el{% endif %}">{{ entry.word }}</div>
<div class="transliteration">{{ entry.transliteration }}{% if entry.pronunciation %} ({{ entry.pronunciation }}){% endif %}</div>
<div class="quick-def">{{ entry.definition }}</div>
</section>
<section class="info-cards">
@@ -661,24 +684,23 @@ function showMoreOccurrences() {
}
(function() {
const allCards = document.querySelectorAll('.occurrence-card');
const visibleCards = document.querySelectorAll('.occurrence-card:not(.hidden)');
if (visibleCards.length === 0) return;
// Include info cards, related entries, and occurrence cards in keyboard nav
function getAllNavigableCards() {
const infoCards = document.querySelectorAll('.info-card');
const related = document.querySelectorAll('.related-entry-card');
const occurrences = document.querySelectorAll('.occurrence-card:not(.hidden)');
return [...infoCards, ...related, ...occurrences];
}
let selectedIndex = -1;
function getVisibleCards() {
return document.querySelectorAll('.occurrence-card:not(.hidden)');
}
function selectCard(index) {
const cards = getVisibleCards();
const cards = getAllNavigableCards();
if (cards.length === 0) return;
// Remove previous selection
if (selectedIndex >= 0) {
const prevCard = document.querySelector('.occurrence-card.selected');
if (prevCard) prevCard.classList.remove('selected');
}
const prevCard = document.querySelector('.info-card.selected, .related-entry-card.selected, .occurrence-card.selected');
if (prevCard) prevCard.classList.remove('selected');
// Update index with bounds checking
selectedIndex = Math.max(0, Math.min(index, cards.length - 1));
@@ -698,7 +720,8 @@ function showMoreOccurrences() {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (typeof KJVNav !== 'undefined' && KJVNav.sidebarActive) return;
const cards = getVisibleCards();
const cards = getAllNavigableCards();
if (cards.length === 0) return;
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
@@ -708,18 +731,27 @@ function showMoreOccurrences() {
selectCard(selectedIndex - 1);
} else if (e.key === 'Enter' && selectedIndex >= 0 && selectedIndex < cards.length) {
e.preventDefault();
const link = cards[selectedIndex].querySelector('.occ-reference');
// Check for link in any card type
const link = cards[selectedIndex].querySelector('.occ-reference, .related-entry-header a, .strongs-ref');
if (link) window.location.href = link.href;
} else if (e.key === '[') {
// Navigate to previous Strong's number
const prevLink = document.querySelector('.nav-btn[rel="prev"]');
if (prevLink) window.location.href = prevLink.href;
} else if (e.key === ']') {
// Navigate to next Strong's number
const nextLink = document.querySelector('.nav-btn[rel="next"]');
if (nextLink) window.location.href = nextLink.href;
}
});
// Click to select
allCards.forEach((card, index) => {
// Click to select - attach to all card types
document.querySelectorAll('.info-card, .related-entry-card, .occurrence-card').forEach((card) => {
card.addEventListener('click', function(e) {
if (e.target.tagName !== 'A') {
const visCards = getVisibleCards();
const visIndex = Array.from(visCards).indexOf(card);
if (visIndex >= 0) selectCard(visIndex);
const allCards = getAllNavigableCards();
const cardIndex = allCards.indexOf(card);
if (cardIndex >= 0) selectCard(cardIndex);
}
});
});