Development: Table of Contents (TOC) Feature
Feature: Dynamic Table of Contents untuk Dokumentasi
Created: 2025-10-27
Status: โ
Completed
Complexity: Medium
๐ Overview
Mengimplementasikan fitur Table of Contents yang dinamis untuk halaman dokumentasi dengan:
- Auto-detection dari HTML headings (h2, h3, h4)
- Active state tracking berdasarkan scroll position
- Auto-scroll untuk menampilkan item aktif
- Dua variant styling (bordered & borderless)
- Smooth navigation dengan hash URL
๐ฏ Objectives
- Membuat komponen TOC yang membaca heading dari rendered markdown
- Track active heading berdasarkan scroll position
- Auto-scroll TOC container saat item aktif berada di luar viewport
- Two styling variants untuk fleksibilitas desain
- Integration dengan halaman docs slug
๐ Development Progress
Initial Request
"buatkan daftar isi untuk docs slug"
User meminta fitur TOC untuk halaman dokumentasi yang menggunakan route /docs/[slug].
Iteration 1: Extract Headings
- Created
TableOfContents.sveltecomponent - Implemented
extractHeadings()function using DOMParser - Extract headings (h2, h3, h4) from HTML content
- Generate slug IDs for each heading
Code:
function extractHeadings(htmlContent: string): TocItem[] {
const headings: TocItem[] = [];
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const headingsElements = doc.querySelectorAll('h2, h3, h4');
// ... extract logic
}
Iteration 2: Active State Detection (Initial - Failed)
- Initial attempt: Used
IntersectionObserver - Problem: Tidak akurat untuk sticky breadcrumb scenario
- Root margin dan threshold tidak reliable
Initial Code (Failed):
const observer = new IntersectionObserver(
(entries) => { /* ... */ },
{
rootMargin: '-100px 0px -70% 0px',
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);
Feedback dari User: "active toc item tidak bekerja dengan benar, tolong perbaiki"
Iteration 3: Scroll-Based Active Detection (Fixed)
- Replaced
IntersectionObserverdengan scroll-based detection - Calculate scroll position dengan offset untuk breadcrumb
- Iterate dari atas ke bawah untuk find current heading
- Implementasi hash change handler
Final Code:
function updateActiveHeading() {
const offset = 150;
const scrollPosition = window.scrollY + offset;
let currentHeading = '';
for (let i = 0; i < tocItems.length; i++) {
const element = document.getElementById(tocItems[i].id);
if (element) {
const elementTop = element.getBoundingClientRect().top + window.scrollY;
if (scrollPosition < elementTop - 100) break;
currentHeading = tocItems[i].id;
}
}
activeId = currentHeading;
}
Iteration 4: Two Variant Styling
- User request: "warna desain kurang menyatu dengan halaman"
- Added
variantprop:'bordered' | 'borderless' - Different styling untuk active state, hover, dan background
- Counter badge hidden untuk borderless variant
User Selection: "saya sudah perbaiki manual, sekarang saya ingin ketika sticky bottom..."
Iteration 5: Auto-Scroll Active Item
- User request: "saat di scroll dan mendapatkan giliran toc active item yang ada di bagian daftar isi yang tidak nampak (baru nampak saat daftar isi di scroll), bisa kita buatkan daftar isi juga bisa scroll otomatis untuk memperlihatkan toc active yang mendapat giliran untuk seharusnya nampak?"
Implementation:
let tocContainer = $state<HTMLElement | null>(null);
$effect(() => {
if (!activeId || !tocContainer) return;
const activeLink = tocContainer.querySelector(`a[href="#${activeId}"]`);
if (activeLink) {
const containerRect = tocContainer.getBoundingClientRect();
const linkRect = activeLink.getBoundingClientRect();
const isAbove = linkRect.top < containerRect.top;
const isBelow = linkRect.bottom > containerRect.bottom;
if (isAbove || isBelow) {
activeLink.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
});
User Feedback: "gilak, keren"
๐ Prompts & User Interactions
Sequence of User Requests
Initial: "buatkan daftar isi untuk docs slug"
- Kebutuhan: Fitur TOC dasar
Refinement: "perbaiki page next dan page previous halaman slugs docs, buatkan menjadi yang ringan dan clean"
- Context: Mengikuti penyesuaian navigasi
UI Design: "keren sekarang tombol daftar isi sudah bekerja sesuai yang kita ekspektasikan, sekarang mari kita perbaiki ui @TableOfContents.svelte untuk tampilan yang lebih clean dan soft"
- Nilai: Clean & soft aesthetic
Remove Bloat: "kita tidak butuh getLevelIcon karena malah akan jadi terlalu bloated"
- Prinsip: Keep it simple, not bloated
Navigation Fix: "buatkan ketika tocItems di tekan maka akan fokus mengarah ke topik tersebut"
- Konsistensi UX
Bug Fix: "saat di klik ke salah satu toc tidak terjadi seperti direct hash yang langsung menuju ke bagian dengan id tersebut"
- Bug fixing request
Bug Fix: "Property 'target' does not exist on type 'never'." (7 times)
- TypeScript error fixing
Styling Variants: "warna desain kurang menyatu dengan halaman, apakah kita bisa dibuatkan opsi bordered seperti timbul dengan card dan borderless seperti menyatu dengan halaman"
- Fleksibilitas desain
Auto-Scroll: "saat di scroll dan mendapatkan giliran toc active item yang ada di bagian daftar isi yang tidak nampak..."
- Indikator interaktif
Bug Fix: "active toc item tidak bekerja dengan benar, tolong perbaiki"
- Perbaikan logic
Final Confirmation: "sekarang active toc item sudah dapat bekerja lagi, terimakasih"
- Testing & approval
๐จ Design Decisions
Styling Variants
Bordered (Card Style):
bg-gray-50+border-gray-200+rounded-xl+p-5+shadow-sm- Active:
bg-indigo-50+text-indigo-700 - Counter badge visible
- Indigo indicator bar
Borderless (Integrated Style):
- Transparent background
p-3(less padding)- Active:
bg-gray-100+text-gray-900 - Counter badge hidden
- Gray indicator bar
Active Detection Strategy
Final Approach: Scroll-based dengan offset calculation
- More predictable dari IntersectionObserver
- Consistent dengan sticky breadcrumb
- Lebih mudah di-debug
- Performance acceptable dengan
passive: true
Auto-Scroll Behavior
- Trigger: Only when active item tidak visible
- Method:
scrollIntoView({ behavior: 'smooth', block: 'center' }) - Prevent scroll jump: Check visibility first
๐งช Testing Scenarios
Tested Cases
- โ Extract headings dari markdown HTML
- โ Generate slug IDs untuk headings
- โ Active state changes saat scroll
- โ Click navigation dengan hash update
- โ Visual highlight saat reach heading
- โ Auto-scroll saat active item outside viewport
- โ Smooth scrolling behavior
- โ Sticky breadcrumb compatibility
- โ Two variant styling
- โ Responsive behavior
Edge Cases
- โ Empty headings list (shows nothing)
- โ Very long TOC (scrollable container)
- โ Heading di posisi extreme (top/bottom)
- โ Multiple rapid scroll events
- โ Hash change vs scroll conflict
๐ Technical Details
Dependencies
- No external library required
- Pure Svelte 5 Runes (
$state,$effect,$derived) - Native DOMParser API
- Native scroll events
Performance
passive: trueuntuk scroll listener- Reactive updates dengan
$state - Minimal DOM queries (cache references)
- Smooth 60fps scrolling dengan
requestAnimationFrame(viascrollIntoView)
Accessibility
- Semantic
<nav>element aria-labeluntuk setiap linktitleattribute untuk truncated text- Keyboard navigable
- Focus ring on keyboard navigation
๐ Metrics & Metrics
Development Time
- Initial implementation: ~15 minutes
- Iterations & refinements: ~45 minutes
- Total: ~60 minutes
Code Complexity
- Lines of code: 252 lines
- Functions: 3 main functions
- Reactive states: 3 states
- Effects: 3 effects
User Feedback Loop
- Requests: 11 total requests
- Bug reports: 8 issues (6 TypeScript errors + 2 functional bugs)
- Design iterations: 4 major iterations
- Approval: 1 explicit approval ("gilak, keren")
๐ฏ Success Criteria Met
- โ TOC extracts headings automatically
- โ Active state tracking works correctly
- โ Smooth navigation dengan hash URL
- โ Auto-scroll untuk visibility
- โ Two variant styling options
- โ Clean & soft aesthetic
- โ Keep it simple, not bloated
- โ Responsive & accessible
- โ Zero external dependencies
๐ก Key Learnings
- IntersectionObserver tidak selalu best solution untuk sticky layouts
- Scroll-based detection lebih predictable dalam edge cases
- User feedback critical untuk UX refinement
- Two variant approach memberikan fleksibilitas tanpa complexity
- Auto-scroll dengan visibility check penting untuk UX
- TypeScript type assertions perlu explicit untuk performance
๐ Related Files
src/lib/components/TableOfContents.svelte- Main componentsrc/routes/docs/[slug]/+page.svelte- Integrationsrc/routes/docs/+page.svelte- Index pagedocs/TECH_CHOICES_RATIONALE.md- Svelte rationale
Last Updated: 2025-10-27