Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 78 additions & 13 deletions site/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,12 @@
var statusClass = p.status.replace(/ /g, '-');
var roman = toRoman(p.id);
var num = String(p.id).padStart(2, '0');
html += '<div class="toc-row" data-phase="' + i + '">';
html += '<span class="toc-num">' + roman + '.</span>';
html += '<div><span class="toc-status ' + statusClass + '"></span><span class="toc-name">' + escapeHtml(p.name) + '</span></div>';
html += '<span class="toc-meta">' + done + ' / ' + total + '</span>';
html += '<span class="toc-meta">' + num + '</span>';
var label = 'Phase ' + num + ': ' + p.name + ', ' + done + ' of ' + total + ' lessons complete';
html += '<div class="toc-row" data-phase="' + i + '" role="button" tabindex="0" aria-label="' + escapeHtml(label) + '">';
html += '<span class="toc-num" aria-hidden="true">' + roman + '.</span>';
html += '<div><span class="toc-status ' + statusClass + '" aria-hidden="true"></span><span class="toc-name">' + escapeHtml(p.name) + '</span></div>';
html += '<span class="toc-meta" aria-hidden="true">' + done + ' / ' + total + '</span>';
html += '<span class="toc-meta" aria-hidden="true">' + num + '</span>';
html += '</div>';
}
grid.innerHTML = html;
Expand Down Expand Up @@ -179,16 +180,34 @@
var row = e.target.closest('.toc-row, .phase-card');
if (row) {
var idx = parseInt(row.getAttribute('data-phase'), 10);
if (!isNaN(idx)) openModal(idx);
if (!isNaN(idx)) openModal(idx, row);
}
});

// Keyboard activation for the role="button" phase rows.
document.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' && e.key !== ' ') return;
var row = e.target.closest && e.target.closest('.toc-row, .phase-card');
if (!row || !row.hasAttribute('data-phase')) return;
e.preventDefault();
var idx = parseInt(row.getAttribute('data-phase'), 10);
if (!isNaN(idx)) openModal(idx, row);
});

closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeModal();
if (!overlay.classList.contains('open')) return;
if (e.key === 'Escape') {
e.preventDefault();
closeModal();
return;
}
if (e.key === 'Tab') {
trapTab(e, overlay);
}
});

var resetBtn = document.getElementById('modalReset');
Expand All @@ -202,21 +221,52 @@
}
}

function getFocusables(root) {
// getClientRects().length is more reliable than offsetParent for visibility:
// offsetParent returns null for position: fixed elements even when visible.
return Array.prototype.slice.call(root.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)).filter(function (el) { return el.getClientRects().length > 0; });
}

function trapTab(e, container) {
var f = getFocusables(container);
if (!f.length) return;
var first = f[0];
var last = f[f.length - 1];
var active = document.activeElement;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
}

var currentPhaseIdx = -1;
var lastModalTrigger = null;

function openModal(idx) {
function openModal(idx, triggerEl) {
var p = PHASES[idx];
if (!p) return;
currentPhaseIdx = idx;
lastModalTrigger = triggerEl || document.activeElement;

document.getElementById('modalPhaseNum').textContent = 'PHASE ' + String(p.id).padStart(2, '0');
document.getElementById('modalTitle').textContent = p.name;
document.getElementById('modalDesc').textContent = p.desc;

renderModalLessons(p);

document.getElementById('modalOverlay').classList.add('open');
var overlay = document.getElementById('modalOverlay');
overlay.classList.add('open');
overlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';

// Move focus into the dialog so screen readers and keyboards land there.
var closeBtn = document.getElementById('modalClose');
if (closeBtn) closeBtn.focus();
}

function renderModalLessons(p) {
Expand All @@ -238,11 +288,13 @@
if (userComplete) statusClass = 'complete';

html += '<div class="modal-lesson' + (userComplete ? ' user-done' : '') + '">';
html += '<span class="modal-lesson-status ' + statusClass + '"' + (userComplete ? ' title="You completed this lesson"' : '') + '></span>';
html += '<span class="modal-lesson-status ' + statusClass + '"' + (userComplete ? ' title="You completed this lesson"' : '') + ' aria-hidden="true"></span>';
if (l.url) {
html += '<a href="' + l.url + '" target="_blank" rel="noopener">' + escapeHtml(l.name) + '</a>';
} else {
html += '<a>' + escapeHtml(l.name) + '</a>';
// Unreleased lesson: render as plain text rather than a non-functional <a>
// so it isn't announced or focused as a link.
html += '<span class="modal-lesson-pending" aria-label="' + escapeHtml(l.name) + ' (not yet available)">' + escapeHtml(l.name) + '</span>';
}
html += '<span class="modal-lesson-type" data-type="' + escapeHtml(l.type) + '"' + (l.combines ? ' title="Combines: ' + escapeHtml(l.combines) + '"' : '') + '>' + escapeHtml(l.type) + '</span>';
html += '<span class="modal-lesson-lang">' + escapeHtml(l.lang) + '</span>';
Expand Down Expand Up @@ -306,21 +358,34 @@
}

function closeModal() {
document.getElementById('modalOverlay').classList.remove('open');
var overlay = document.getElementById('modalOverlay');
overlay.classList.remove('open');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
if (lastModalTrigger && typeof lastModalTrigger.focus === 'function') {
try { lastModalTrigger.focus(); } catch (_) {}
}
lastModalTrigger = null;
}

function initCopyButton() {
var btn = document.getElementById('copyBtn');
var code = document.getElementById('cloneCmd');
var status = document.getElementById('copyStatus');
if (!btn || !code) return;
var originalLabel = btn.textContent;
var revertTimer = null;
btn.addEventListener('click', function () {
navigator.clipboard.writeText(code.textContent).then(function () {
btn.textContent = '✓';
// Announce via a dedicated live region — the button's aria-label
// overrides its textContent, so AT won't hear "✓" otherwise.
if (status) status.textContent = 'Command copied to clipboard';
if (revertTimer) clearTimeout(revertTimer);
revertTimer = setTimeout(function () { btn.textContent = originalLabel; }, 1500);
revertTimer = setTimeout(function () {
btn.textContent = originalLabel;
if (status) status.textContent = '';
}, 1500);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
}
Expand Down
62 changes: 40 additions & 22 deletions site/catalog.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,28 @@
}

.catalog-table th {
text-align: left;
padding: 0;
border-bottom: 2px solid var(--border);
white-space: nowrap;
}

.catalog-table th .sort-btn {
width: 100%;
text-align: left;
background: none;
border: 0;
padding: 14px 16px;
font-family: var(--font-heading);
font-weight: 700;
font-size: 0.9rem;
text-align: left;
padding: 14px 16px;
border-bottom: 2px solid var(--border);
color: var(--text-muted);
cursor: pointer;
user-select: none;
white-space: nowrap;
transition: color 0.2s;
}

.catalog-table th:hover {
.catalog-table th .sort-btn:hover {
color: var(--accent);
}

Expand Down Expand Up @@ -229,10 +237,10 @@
<a href="catalog.html">Catalog</a>
<a href="prereqs.html">Roadmap</a>
<a href="glossary.html">Glossary</a>
<a href="https://github.com/rohitg00/ai-engineering-from-scratch" target="_blank" rel="noopener" class="header-github">
<a href="https://github.com/rohitg00/ai-engineering-from-scratch" target="_blank" rel="noopener" class="header-github" aria-label="GitHub repository (opens in new tab)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.4 3-.405 1.02.005 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
<svg class="star-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279L12 19.896l-7.416 3.517 1.48-8.279L0 9.306l8.332-1.151z"/></svg>
<span class="star-count" data-loading="true" aria-label="GitHub stars">…</span>
<span class="star-count" data-loading="true" aria-hidden="true">…</span>
</a>
</nav>
<button class="search-toggle" type="button" data-cmd-palette
Expand All @@ -245,7 +253,7 @@
</svg>
</button>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme" type="button">
<span class="theme-icon" id="themeIcon">N</span>
<span class="theme-icon" id="themeIcon" aria-hidden="true">N</span>
</button>
</div>
</header>
Expand All @@ -257,26 +265,29 @@ <h1>Lesson Catalog</h1>
<p>Every lesson across all 20 phases. Search, filter, sort.</p>
</div>
<div class="catalog-controls">
<input type="text" class="catalog-search" id="catalogSearch" placeholder="Search lessons...">
<select class="catalog-filter" id="catalogPhase">
<label for="catalogSearch" class="sr-only">Search lessons</label>
<input type="search" class="catalog-search" id="catalogSearch" placeholder="Search lessons..." aria-controls="catalogTable">
<label for="catalogPhase" class="sr-only">Filter by phase</label>
<select class="catalog-filter" id="catalogPhase" aria-controls="catalogTable">
<option value="">All Phases</option>
</select>
<select class="catalog-filter" id="catalogStatus">
<label for="catalogStatus" class="sr-only">Filter by status</label>
<select class="catalog-filter" id="catalogStatus" aria-controls="catalogTable">
<option value="">All Status</option>
<option value="complete">Complete</option>
<option value="planned">Planned</option>
</select>
</div>
<div class="catalog-count" id="catalogCount"></div>
<div class="catalog-count" id="catalogCount" role="status" aria-live="polite" aria-atomic="true"></div>
<div class="catalog-table-wrap">
<table class="catalog-table" id="catalogTable">
<thead>
<tr>
<th data-sort="phase">Phase <span class="sort-arrow"></span></th>
<th data-sort="name">Lesson <span class="sort-arrow"></span></th>
<th data-sort="type">Type <span class="sort-arrow"></span></th>
<th data-sort="lang">Language <span class="sort-arrow"></span></th>
<th data-sort="status">Status <span class="sort-arrow"></span></th>
<th data-sort="phase" aria-sort="none"><button type="button" class="sort-btn">Phase <span class="sort-arrow" aria-hidden="true"></span></button></th>
<th data-sort="name" aria-sort="none"><button type="button" class="sort-btn">Lesson <span class="sort-arrow" aria-hidden="true"></span></button></th>
<th data-sort="type" aria-sort="none"><button type="button" class="sort-btn">Type <span class="sort-arrow" aria-hidden="true"></span></button></th>
<th data-sort="lang" aria-sort="none"><button type="button" class="sort-btn">Language <span class="sort-arrow" aria-hidden="true"></span></button></th>
<th data-sort="status" aria-sort="none"><button type="button" class="sort-btn">Status <span class="sort-arrow" aria-hidden="true"></span></button></th>
</tr>
</thead>
<tbody id="catalogBody"></tbody>
Expand Down Expand Up @@ -448,20 +459,27 @@ <h1>Lesson Catalog</h1>
statusSelect.addEventListener('change', render);

document.querySelectorAll('.catalog-table th[data-sort]').forEach(function (th) {
th.addEventListener('click', function () {
var btn = th.querySelector('.sort-btn');
var handler = function () {
var col = th.getAttribute('data-sort');
if (sortCol === col) {
sortDir = sortDir * -1;
} else {
sortCol = col;
sortDir = 1;
}
document.querySelectorAll('.catalog-table th .sort-arrow').forEach(function (a) {
a.textContent = '';
document.querySelectorAll('.catalog-table th[data-sort]').forEach(function (otherTh) {
otherTh.setAttribute('aria-sort', 'none');
var arrow = otherTh.querySelector('.sort-arrow');
if (arrow) arrow.textContent = '';
});
th.querySelector('.sort-arrow').textContent = sortDir === 1 ? ' \u25B2' : ' \u25BC';
th.setAttribute('aria-sort', sortDir === 1 ? 'ascending' : 'descending');
var arrow = th.querySelector('.sort-arrow');
if (arrow) arrow.textContent = sortDir === 1 ? ' \u25B2' : ' \u25BC';
render();
});
};
if (btn) btn.addEventListener('click', handler);
else th.addEventListener('click', handler);
});

render();
Expand Down
11 changes: 6 additions & 5 deletions site/glossary.html
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,10 @@
<a href="catalog.html">Catalog</a>
<a href="prereqs.html">Roadmap</a>
<a href="glossary.html">Glossary</a>
<a href="https://github.com/rohitg00/ai-engineering-from-scratch" target="_blank" rel="noopener" class="header-github">
<a href="https://github.com/rohitg00/ai-engineering-from-scratch" target="_blank" rel="noopener" class="header-github" aria-label="GitHub repository (opens in new tab)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.4 3-.405 1.02.005 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
<svg class="star-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279L12 19.896l-7.416 3.517 1.48-8.279L0 9.306l8.332-1.151z"/></svg>
<span class="star-count" data-loading="true" aria-label="GitHub stars">…</span>
<span class="star-count" data-loading="true" aria-hidden="true">…</span>
</a>
</nav>
<button class="search-toggle" type="button" data-cmd-palette
Expand All @@ -196,7 +196,7 @@
</svg>
</button>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme" type="button">
<span class="theme-icon" id="themeIcon">N</span>
<span class="theme-icon" id="themeIcon" aria-hidden="true">N</span>
</button>
</div>
</header>
Expand All @@ -208,9 +208,10 @@ <h1>AI Glossary</h1>
<p>What people <em>say</em> vs what things actually <em>mean</em></p>
</div>
<div class="glossary-search-wrap">
<input type="text" class="glossary-search" id="glossarySearch" placeholder="Search terms...">
<label for="glossarySearch" class="sr-only">Search glossary terms</label>
<input type="search" class="glossary-search" id="glossarySearch" placeholder="Search terms..." aria-controls="glossaryList">
</div>
<div class="glossary-count" id="glossaryCount"></div>
<div class="glossary-count" id="glossaryCount" role="status" aria-live="polite" aria-atomic="true"></div>
<div class="glossary-list" id="glossaryList"></div>
</div>
</main>
Expand Down
Loading