Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions src/js/08-static-renders.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@
'adding to the very problem it tracks.';
}

// ---- Add anchor links to section headings -------------------
function renderSectionAnchors() {
document.querySelectorAll('section[id]').forEach((section) => {
const h2 = section.querySelector('h2');
if (!h2) return;
if (h2.querySelector('.section-anchor')) return;
const anchor = document.createElement('a');
anchor.className = 'section-anchor';
anchor.href = '#' + section.id;
anchor.setAttribute('aria-label', 'Link to section: ' + section.id);
anchor.textContent = '#';
h2.appendChild(anchor);
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ============================================================
// FUN FEATURES
// ============================================================
Expand Down
1 change: 1 addition & 0 deletions src/js/21-boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
renderTips();
renderChangelog();
renderFooterStats();
renderSectionAnchors();

// Chart init is isolated so a missing date-adapter or other chart error
// cannot prevent the counters and life-blocks from running.
Expand Down
23 changes: 23 additions & 0 deletions styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ h1 { font-size: clamp(1.8rem, 5vw, 3.2rem); font-weight: 900; }
h2 { font-size: clamp(1.2rem, 3vw, 1.8rem); font-weight: 700; margin-bottom: 1rem; }
h3 { font-size: 1rem; font-weight: 700; }

/* ---- Section anchor links ---- */
.section-anchor {
display: inline-block;
margin-left: 0.5em;
font-size: 0.7em;
color: var(--text-dim);
text-decoration: none;
opacity: 0;
transition: opacity 0.2s;
vertical-align: middle;
user-select: none;
}
h2:hover .section-anchor,
h2:focus-within .section-anchor,
.section-anchor:focus {
opacity: 1;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
@media (hover: none) {
.section-anchor {
opacity: 1;
}
}
Comment thread
nitrocode marked this conversation as resolved.

/* ---- Layout ---- */
.container {
max-width: 1200px;
Expand Down
24 changes: 24 additions & 0 deletions tests/e2e/death-clock.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,30 @@ test.describe('AI Death Clock — end-to-end', () => {
expect(html).not.toContain('<script>');
expect(html).not.toContain('javascript:');
});

// ── Section anchor links ──────────────────────────────────────────────────

test('section headings have anchor links rendered', async ({ page }) => {
// Each section with an id should have a .section-anchor child in its h2
const anchors = await page.locator('section[id] h2 .section-anchor').all();
expect(anchors.length).toBeGreaterThan(0);
});

test('section anchor href matches the section id', async ({ page }) => {
const sectionId = 'counter-section';
const href = await page.locator(`#${sectionId} h2 .section-anchor`).getAttribute('href');
expect(href).toBe(`#${sectionId}`);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('navigating to a section anchor deep-link activates the correct tab', async ({ page }) => {
await page.goto('/#milestones-section');
await page.waitForLoadState('networkidle');
// milestones-section lives in the dashboard tab; it should be visible (not hidden)
const section = page.locator('#milestones-section');
await expect(section).toBeVisible();
// The dashboard tab panel should not be hidden
await expect(page.locator('#tab-dashboard')).not.toHaveAttribute('hidden', /.*/);
});
});

// ── Mobile layout: fixed elements must stay within the viewport ───────────
Expand Down
Loading