Skip to content

Commit edf6441

Browse files
UX improvements and fix broken markdown tables (#9)
UX changes: - ToC right rail: active heading now has a left-border indicator - Keyboard nav: ArrowLeft/ArrowRight navigate between chapters - Reading progress bar: 2px height with stronger contrast - Search: ArrowUp/ArrowDown/Enter keyboard navigation in results - Mobile sidebar: overlay fades in/out in sync with slide transition Content fixes: - Fix markdown tables with rows wrapping across multiple lines in 02-capability-jump, 27-enterprise-adoption, 32-backpressure, 33-adoption-playbook, and appendix-b-glossary Co-authored-by: Ona <no-reply@ona.com>
1 parent 154f87b commit edf6441

9 files changed

Lines changed: 190 additions & 135 deletions

src/components/ChapterNav.astro

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@ interface Props {
99
const { chapters, currentSlug } = Astro.props;
1010
const { prev, next } = getAdjacentChapters(chapters, currentSlug);
1111
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
12+
13+
const prevUrl = prev ? `${base}/${prev.slug}/` : null;
14+
const nextUrl = next ? `${base}/${next.slug}/` : null;
1215
---
1316

14-
<nav class="mt-16 pt-8 border-t border-[var(--border)] flex justify-between gap-4">
17+
<nav
18+
class="mt-16 pt-8 border-t border-[var(--border)] flex justify-between gap-4"
19+
data-chapter-nav
20+
data-prev={prevUrl}
21+
data-next={nextUrl}
22+
>
1523
{prev ? (
16-
<a href={`${base}/${prev.slug}/`} class="group flex flex-col items-start gap-1 max-w-[45%]">
24+
<a href={prevUrl} class="group flex flex-col items-start gap-1 max-w-[45%]">
1725
<span class="text-xs text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)] transition-colors">
1826
← Previous
1927
</span>
@@ -23,7 +31,7 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
2331
</a>
2432
) : <div />}
2533
{next ? (
26-
<a href={`${base}/${next.slug}/`} class="group flex flex-col items-end gap-1 max-w-[45%] ml-auto">
34+
<a href={nextUrl} class="group flex flex-col items-end gap-1 max-w-[45%] ml-auto">
2735
<span class="text-xs text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)] transition-colors">
2836
Next →
2937
</span>
@@ -33,3 +41,25 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
3341
</a>
3442
) : <div />}
3543
</nav>
44+
45+
<script>
46+
// Keyboard navigation: ArrowLeft/ArrowRight for prev/next chapter
47+
const nav = document.querySelector('[data-chapter-nav]');
48+
if (nav) {
49+
const prevUrl = nav.getAttribute('data-prev');
50+
const nextUrl = nav.getAttribute('data-next');
51+
52+
document.addEventListener('keydown', (e) => {
53+
// Don't navigate when user is typing in an input/textarea or search is open
54+
const tag = (e.target as HTMLElement).tagName;
55+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
56+
if ((e.target as HTMLElement).isContentEditable) return;
57+
58+
if (e.key === 'ArrowLeft' && prevUrl) {
59+
window.location.href = prevUrl;
60+
} else if (e.key === 'ArrowRight' && nextUrl) {
61+
window.location.href = nextUrl;
62+
}
63+
});
64+
}
65+
</script>

src/components/Header.astro

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
44

55
<header class="fixed top-0 left-0 right-0 z-50 h-14 border-b border-[var(--border-subtle)] bg-[var(--bg-primary)]/80 backdrop-blur-md">
66
<!-- Reading progress bar -->
7-
<div id="reading-progress" class="absolute bottom-0 left-0 h-[1px] bg-[var(--text-tertiary)] w-full scale-x-0 origin-left"></div>
7+
<div id="reading-progress" class="absolute bottom-0 left-0 h-[2px] bg-[var(--text-secondary)] w-full scale-x-0 origin-left"></div>
88

99
<div class="h-full flex items-center justify-between px-4 lg:px-6">
1010
<!-- Left: logo + mobile menu -->
@@ -114,12 +114,28 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
114114
localStorage.setItem('theme', isDark ? 'light' : 'dark');
115115
});
116116

117-
// Mobile sidebar toggle
117+
// Mobile sidebar toggle — fade overlay in sync with slide
118118
document.getElementById('sidebar-toggle')?.addEventListener('click', function() {
119119
var sidebar = document.getElementById('sidebar');
120120
var overlay = document.getElementById('sidebar-overlay');
121-
sidebar?.classList.toggle('-translate-x-full');
122-
overlay?.classList.toggle('hidden');
121+
if (!sidebar || !overlay) return;
122+
123+
var isHidden = sidebar.classList.contains('-translate-x-full');
124+
if (isHidden) {
125+
// Opening: show overlay first, then fade in
126+
overlay.classList.remove('hidden');
127+
requestAnimationFrame(function() {
128+
overlay.classList.remove('opacity-0');
129+
});
130+
sidebar.classList.remove('-translate-x-full');
131+
} else {
132+
// Closing: fade out overlay, then hide
133+
sidebar.classList.add('-translate-x-full');
134+
overlay.classList.add('opacity-0');
135+
overlay.addEventListener('transitionend', function() {
136+
overlay.classList.add('hidden');
137+
}, { once: true });
138+
}
123139
});
124140

125141
// Reading progress
@@ -169,20 +185,59 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
169185
}
170186
}
171187

188+
var searchSelectedIndex = -1;
189+
190+
function updateSearchSelection() {
191+
var items = searchResults ? searchResults.querySelectorAll('[data-search-result]') : [];
192+
items.forEach(function(item, i) {
193+
if (i === searchSelectedIndex) {
194+
item.classList.add('bg-[var(--bg-hover)]');
195+
} else {
196+
item.classList.remove('bg-[var(--bg-hover)]');
197+
}
198+
});
199+
// Scroll selected item into view within the results container
200+
if (searchSelectedIndex >= 0 && items[searchSelectedIndex]) {
201+
items[searchSelectedIndex].scrollIntoView({ block: 'nearest' });
202+
}
203+
}
204+
172205
searchTrigger?.addEventListener('click', openSearch);
173206
searchBackdrop?.addEventListener('click', closeSearch);
174207

175208
document.addEventListener('keydown', function(e) {
176209
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
177210
e.preventDefault();
178211
searchDialog?.classList.contains('hidden') ? openSearch() : closeSearch();
212+
return;
213+
}
214+
if (e.key === 'Escape') { closeSearch(); return; }
215+
216+
// Arrow key navigation within search results
217+
var isSearchOpen = searchDialog && !searchDialog.classList.contains('hidden');
218+
if (!isSearchOpen || !searchResults) return;
219+
220+
var items = searchResults.querySelectorAll('[data-search-result]');
221+
if (items.length === 0) return;
222+
223+
if (e.key === 'ArrowDown') {
224+
e.preventDefault();
225+
searchSelectedIndex = Math.min(searchSelectedIndex + 1, items.length - 1);
226+
updateSearchSelection();
227+
} else if (e.key === 'ArrowUp') {
228+
e.preventDefault();
229+
searchSelectedIndex = Math.max(searchSelectedIndex - 1, 0);
230+
updateSearchSelection();
231+
} else if (e.key === 'Enter' && searchSelectedIndex >= 0 && items[searchSelectedIndex]) {
232+
e.preventDefault();
233+
items[searchSelectedIndex].click();
179234
}
180-
if (e.key === 'Escape') closeSearch();
181235
});
182236

183237
var debounceTimer;
184238
searchInput?.addEventListener('input', function() {
185239
clearTimeout(debounceTimer);
240+
searchSelectedIndex = -1;
186241
debounceTimer = setTimeout(async function() {
187242
var query = searchInput.value.trim();
188243
if (!searchResults) return;
@@ -210,11 +265,20 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
210265
}
211266

212267
searchResults.innerHTML = results.map(function(r) {
213-
return '<a href="' + r.url + '" class="block px-3 py-2.5 rounded-md hover:bg-[var(--bg-hover)] transition-colors">' +
268+
return '<a href="' + r.url + '" data-search-result class="block px-3 py-2.5 rounded-md transition-colors">' +
214269
'<span class="block text-sm font-medium text-[var(--text-primary)] leading-snug">' + (r.meta?.title || 'Untitled') + '</span>' +
215270
'<span class="block text-xs text-[var(--text-tertiary)] mt-1 leading-relaxed line-clamp-2">' + (r.excerpt || '') + '</span>' +
216271
'</a>';
217272
}).join('');
273+
274+
// Reset selection and add hover listeners
275+
searchSelectedIndex = -1;
276+
searchResults.querySelectorAll('[data-search-result]').forEach(function(item, i) {
277+
item.addEventListener('mouseenter', function() {
278+
searchSelectedIndex = i;
279+
updateSearchSelection();
280+
});
281+
});
218282
}, 200);
219283
});
220284
</script>

src/components/Sidebar.astro

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
1212
---
1313

1414
<!-- Mobile overlay -->
15-
<div id="sidebar-overlay" class="fixed inset-0 z-30 bg-black/40 backdrop-blur-sm lg:hidden hidden"></div>
15+
<div
16+
id="sidebar-overlay"
17+
class="fixed inset-0 z-30 bg-black/40 backdrop-blur-sm lg:hidden hidden opacity-0 transition-opacity duration-200"
18+
></div>
1619

1720
<aside
1821
id="sidebar"
@@ -73,23 +76,28 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
7376
// Scroll the active sidebar link into view on page load
7477
const activeLink = document.querySelector('#sidebar a[data-active]');
7578
if (activeLink) {
76-
// Use requestAnimationFrame to ensure layout is complete before scrolling
7779
requestAnimationFrame(() => {
7880
activeLink.scrollIntoView({ block: 'center', behavior: 'instant' });
7981
});
8082
}
8183

84+
// Mobile sidebar: close with synced overlay fade
85+
function closeMobileSidebar() {
86+
if (window.innerWidth >= 1024) return;
87+
const sidebar = document.getElementById('sidebar');
88+
const overlay = document.getElementById('sidebar-overlay');
89+
if (!sidebar || !overlay) return;
90+
91+
sidebar.classList.add('-translate-x-full');
92+
overlay.classList.add('opacity-0');
93+
overlay.addEventListener('transitionend', () => {
94+
overlay.classList.add('hidden');
95+
}, { once: true });
96+
}
97+
8298
document.querySelectorAll('#sidebar a').forEach(link => {
83-
link.addEventListener('click', () => {
84-
if (window.innerWidth < 1024) {
85-
document.getElementById('sidebar')?.classList.add('-translate-x-full');
86-
document.getElementById('sidebar-overlay')?.classList.add('hidden');
87-
}
88-
});
99+
link.addEventListener('click', closeMobileSidebar);
89100
});
90101

91-
document.getElementById('sidebar-overlay')?.addEventListener('click', () => {
92-
document.getElementById('sidebar')?.classList.add('-translate-x-full');
93-
document.getElementById('sidebar-overlay')?.classList.add('hidden');
94-
});
102+
document.getElementById('sidebar-overlay')?.addEventListener('click', closeMobileSidebar);
95103
</script>

src/components/TableOfContents.astro

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ const filtered = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
1515
<span class="font-[var(--font-mono)] text-[10px] tracking-widest uppercase text-[var(--text-tertiary)] mb-3 block">
1616
On this page
1717
</span>
18-
<ul class="space-y-1.5">
18+
<ul class="relative border-l border-[var(--border-subtle)] space-y-0.5">
1919
{filtered.map((h) => (
2020
<li>
2121
<a
2222
href={`#${h.slug}`}
2323
class:list={[
24-
"block text-xs leading-relaxed transition-colors hover:text-[var(--text-primary)]",
25-
h.depth === 2 ? "text-[var(--text-secondary)]" : "text-[var(--text-tertiary)] pl-3",
24+
"block text-xs leading-relaxed transition-colors duration-150 hover:text-[var(--text-primary)] py-1 -ml-px border-l-2 border-transparent",
25+
h.depth === 2 ? "text-[var(--text-secondary)] pl-3" : "text-[var(--text-tertiary)] pl-5",
2626
]}
2727
data-toc-link
2828
data-toc-slug={h.slug}
@@ -69,19 +69,23 @@ const filtered = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
6969
<script>
7070
const tocLinks = document.querySelectorAll('[data-toc-link]');
7171
if (tocLinks.length > 0) {
72+
function setActiveTocLink(slug: string) {
73+
tocLinks.forEach(link => {
74+
link.classList.remove('!text-[var(--text-primary)]', 'font-medium', '!border-[var(--text-primary)]');
75+
link.classList.add('border-transparent');
76+
});
77+
const active = document.querySelector(`[data-toc-slug="${slug}"]`);
78+
if (active) {
79+
active.classList.add('!text-[var(--text-primary)]', 'font-medium', '!border-[var(--text-primary)]');
80+
active.classList.remove('border-transparent');
81+
}
82+
}
83+
7284
const observer = new IntersectionObserver(
7385
(entries) => {
7486
for (const entry of entries) {
7587
if (entry.isIntersecting) {
76-
tocLinks.forEach(link => {
77-
link.classList.remove('!text-[var(--text-primary)]');
78-
link.classList.remove('font-medium');
79-
});
80-
const active = document.querySelector(`[data-toc-slug="${entry.target.id}"]`);
81-
if (active) {
82-
active.classList.add('!text-[var(--text-primary)]');
83-
active.classList.add('font-medium');
84-
}
88+
setActiveTocLink(entry.target.id);
8589
}
8690
}
8791
},

src/content/chapters/02-capability-jump.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,7 @@ provider charges per million tokens, with output tokens costing 3-5x
315315
more than input tokens. Understanding the pricing structure helps you
316316
make informed decisions about model routing and cost optimization.
317317

318-
| Model | Input $/1M tokens | Output $/1M tokens | Effective cost
319-
for 100K input + 10K output |
318+
| Model | Input $/1M tokens | Output $/1M tokens | Effective cost for 100K input + 10K output |
320319
| --- | --- | --- | --- |
321320
| GPT-5.2 | $1.75 | $14.00 | $0.315 |
322321
| GPT-5.2-mini | $0.15 | $0.60 | $0.021 |

src/content/chapters/27-enterprise-adoption.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ resistance. It’s also a trap.
5050

5151
| Cloud Provider | AI Service | Lock-in Mechanism |
5252
| --- | --- | --- |
53-
| **Microsoft Azure** | Azure OpenAI | Enterprise agreements, Active Directory integration, compliance
54-
certifications |
53+
| **Microsoft Azure** | Azure OpenAI | Enterprise agreements, Active Directory integration, compliance certifications |
5554
| **AWS** | Bedrock | VPC integration, IAM policies, data residency |
5655
| **Google Cloud** | Vertex AI | BigQuery integration, TPU access, Gemini exclusivity |
5756

src/content/chapters/32-backpressure.mdx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,9 @@ Backpressure isn’t binary. It’s a dial.
180180

181181
| Setting | Symptom | Fix |
182182
| --- | --- | --- |
183-
| **Too little** | Hallucinations pass through. Agent output looks clean but is subtly
184-
wrong. Human catches logic errors. | Add more automated checks. Strengthen test coverage. |
185-
| **Too much** | Feedback loop is too slow. Agent waits 20 min for tests. Speed
186-
advantage lost. | Parallelize checks. Run only affected tests. Cache build artifacts. |
187-
| **Just right** | Agent self-corrects in 1-3 iterations. Human reviews architecture and
188-
judgment calls only. | Target: full cycle in < 2 minutes. |
183+
| **Too little** | Hallucinations pass through. Agent output looks clean but is subtly wrong. Human catches logic errors. | Add more automated checks. Strengthen test coverage. |
184+
| **Too much** | Feedback loop is too slow. Agent waits 20 min for tests. Speed advantage lost. | Parallelize checks. Run only affected tests. Cache build artifacts. |
185+
| **Just right** | Agent self-corrects in 1-3 iterations. Human reviews architecture and judgment calls only. | Target: full cycle in < 2 minutes. |
189186

190187
## Measuring backpressure effectiveness
191188
How do you know if your backpressure is working? Track three metrics.

src/content/chapters/33-adoption-playbook.mdx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,9 @@ which means the agent is creating work rather than saving it.
126126
| Objection | Response |
127127
| --- | --- |
128128
| "I can write it faster myself" | "For this task, yes. Track it for a week and compare." |
129-
| "I don’t trust AI code" | "That’s why we have backpressure. The agent self-corrects before you
130-
see it." |
129+
| "I don’t trust AI code" | "That’s why we have backpressure. The agent self-corrects before you see it." |
131130
| "It’ll make us lazy" | "Morning no-AI sessions keep skills sharp. Chapter 20 covers this." |
132-
| "What about security?" | "Agents run in sandboxes with scoped permissions. Chapter 7-10 covers
133-
this." |
131+
| "What about security?" | "Agents run in sandboxes with scoped permissions. Chapter 7-10 covers this." |
134132

135133
## Weeks 5-8: Scale
136134
**Goal:** Agents handle routine work. Humans focus on architecture

0 commit comments

Comments
 (0)