This commit is contained in:
2025-11-16 18:01:30 +01:00
commit 858003cb0b
26 changed files with 4712 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
// Course List Component
export class CourseList {
constructor(onSectionClick) {
this.container = document.getElementById('courseList');
this.onSectionClick = onSectionClick;
this.courseData = null;
}
async loadCourseContent() {
if (this.container?.querySelector('.course-loaded')) {
return; // Already loaded
}
try {
const response = await fetch('/course');
const data = await response.json();
this.courseData = data;
const list = document.createElement('div');
list.className = 'module-items';
list.classList.add('course-loaded');
if (data.sections && data.sections.length > 0) {
data.sections.forEach(section => {
const sectionDiv = this.createSectionItem(section);
list.appendChild(sectionDiv);
});
}
if (this.container) {
this.container.innerHTML = '';
this.container.appendChild(list);
}
} catch (error) {
if (this.container) {
this.container.innerHTML = `<div class="error-message">Error loading course: ${error.message}</div>`;
}
}
}
createSectionItem(section) {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'module-section';
const sectionTitle = document.createElement('h4');
sectionTitle.textContent = section.title;
sectionTitle.className = 'module-section-title';
sectionDiv.appendChild(sectionTitle);
const itemsList = document.createElement('div');
itemsList.className = 'module-items-list';
// Create clickable navigation item
const navBtn = document.createElement('button');
navBtn.className = 'module-item';
navBtn.textContent = section.title;
navBtn.addEventListener('click', () => {
if (this.onSectionClick) {
this.onSectionClick(section);
}
});
itemsList.appendChild(navBtn);
// Add subsections if any
if (section.subsections && section.subsections.length > 0) {
section.subsections.forEach(subsection => {
const subBtn = document.createElement('button');
subBtn.className = 'module-item';
subBtn.style.paddingLeft = '1.5rem';
subBtn.textContent = subsection.title;
subBtn.addEventListener('click', () => {
if (this.onSectionClick) {
this.onSectionClick(subsection);
}
});
itemsList.appendChild(subBtn);
});
}
sectionDiv.appendChild(itemsList);
return sectionDiv;
}
getCourseData() {
return this.courseData;
}
}

View File

@@ -0,0 +1,134 @@
// Language Selector Component with Emojis
export class LanguageSelector {
constructor(onLanguageChange) {
this.onLanguageChange = onLanguageChange;
this.currentLang = '';
this.languages = [
{ code: '', name: 'English', emoji: '🇬🇧', native: 'English' },
{ code: 'de', name: 'German', emoji: '🇩🇪', native: 'Deutsch' },
{ code: 'fr', name: 'French', emoji: '🇫🇷', native: 'Français' },
{ code: 'es', name: 'Spanish', emoji: '🇪🇸', native: 'Español' },
{ code: 'it', name: 'Italian', emoji: '🇮🇹', native: 'Italiano' },
{ code: 'pt', name: 'Portuguese', emoji: '🇵🇹', native: 'Português' },
{ code: 'ru', name: 'Russian', emoji: '🇷🇺', native: 'Русский' }
];
this.init();
}
init() {
// Load saved language preference
const savedLang = localStorage.getItem('preferredLanguage') || '';
this.currentLang = savedLang;
this.createSelector();
}
createSelector() {
// Create floating language selector
const selector = document.createElement('div');
selector.className = 'language-selector';
selector.id = 'languageSelector';
const button = document.createElement('button');
button.className = 'language-selector-button';
button.setAttribute('aria-label', 'Select language');
button.setAttribute('aria-haspopup', 'true');
const currentLang = this.languages.find(lang => lang.code === this.currentLang) || this.languages[0];
button.innerHTML = `
<span class="language-emoji">${currentLang.emoji}</span>
<span class="language-name">${currentLang.native}</span>
<span class="language-arrow">▼</span>
`;
const dropdown = document.createElement('div');
dropdown.className = 'language-dropdown';
dropdown.id = 'languageDropdown';
this.languages.forEach(lang => {
const option = document.createElement('button');
option.className = `language-option ${lang.code === this.currentLang ? 'active' : ''}`;
option.dataset.code = lang.code;
option.innerHTML = `
<span class="language-emoji">${lang.emoji}</span>
<span class="language-name">${lang.native}</span>
<span class="language-name-en">${lang.name}</span>
`;
option.addEventListener('click', () => {
this.selectLanguage(lang.code);
dropdown.classList.remove('active');
});
dropdown.appendChild(option);
});
button.addEventListener('click', (e) => {
e.stopPropagation();
const isActive = dropdown.classList.contains('active');
dropdown.classList.toggle('active');
// Update arrow rotation
const arrow = button.querySelector('.language-arrow');
if (arrow) {
arrow.style.transform = isActive ? 'rotate(0deg)' : 'rotate(180deg)';
}
});
selector.appendChild(button);
selector.appendChild(dropdown);
// Store reference for arrow rotation
this.button = button;
this.dropdown = dropdown;
// Insert into content area (top right)
const content = document.getElementById('content');
if (content) {
content.insertBefore(selector, content.firstChild);
}
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!selector.contains(e.target)) {
dropdown.classList.remove('active');
const arrow = button.querySelector('.language-arrow');
if (arrow) {
arrow.style.transform = 'rotate(0deg)';
}
}
});
}
selectLanguage(code) {
this.currentLang = code;
localStorage.setItem('preferredLanguage', code);
// Update button display
const currentLang = this.languages.find(lang => lang.code === code) || this.languages[0];
const button = document.querySelector('.language-selector-button');
if (button) {
button.innerHTML = `
<span class="language-emoji">${currentLang.emoji}</span>
<span class="language-name">${currentLang.native}</span>
<span class="language-arrow">▼</span>
`;
}
// Update active option
document.querySelectorAll('.language-option').forEach(option => {
if (option.dataset.code === code) {
option.classList.add('active');
} else {
option.classList.remove('active');
}
});
if (this.onLanguageChange) {
this.onLanguageChange(code);
}
}
getCurrentLanguage() {
return this.currentLang;
}
}

View File

@@ -0,0 +1,158 @@
// Loading Progress Component with Qt-style progress bar
export class LoadingProgress {
constructor(container) {
this.container = container;
this.progressBar = null;
this.progressFill = null;
this.animationFrame = null;
this.progress = 0;
this.targetProgress = 0;
this.isAnimating = false;
}
show(message = 'Loading...', showProgress = true) {
if (!this.container) return;
const loadingHTML = `
<div class="loading-container">
<div class="loading-spinner">
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
</div>
<div class="loading-text">${message}</div>
${showProgress ? `
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
<div class="progress-shine"></div>
</div>
<div class="progress-text" id="progressText">0%</div>
</div>
` : ''}
</div>
`;
this.container.innerHTML = loadingHTML;
if (showProgress) {
this.progressBar = this.container.querySelector('.progress-bar');
this.progressFill = document.getElementById('progressFill');
this.progressText = document.getElementById('progressText');
this.startProgressAnimation();
}
}
startProgressAnimation() {
this.progress = 0;
this.targetProgress = 0;
this.isAnimating = true;
// More stable progress simulation with smoother steps
const steps = [
{ progress: 8, delay: 150 },
{ progress: 20, delay: 250 },
{ progress: 35, delay: 400 },
{ progress: 50, delay: 350 },
{ progress: 65, delay: 500 },
{ progress: 78, delay: 450 },
{ progress: 88, delay: 600 },
{ progress: 95, delay: 700 },
];
let stepIndex = 0;
let isCancelled = false;
this.cancelProgress = () => { isCancelled = true; };
const updateProgress = () => {
if (isCancelled || !this.isAnimating) return;
if (stepIndex < steps.length) {
const step = steps[stepIndex];
this.targetProgress = step.progress;
stepIndex++;
setTimeout(updateProgress, step.delay);
} else {
// Keep at 95% until actual loading completes
this.targetProgress = 95;
}
};
updateProgress();
this.animateProgress();
}
animateProgress() {
if (!this.isAnimating || !this.progressFill) return;
// Smooth interpolation
const diff = this.targetProgress - this.progress;
if (Math.abs(diff) > 0.1) {
this.progress += diff * 0.15; // Smooth easing
} else {
this.progress = this.targetProgress;
}
if (this.progressFill) {
this.progressFill.style.width = `${this.progress}%`;
}
if (this.progressText) {
this.progressText.textContent = `${Math.round(this.progress)}%`;
}
// Add class when progress starts
if (this.progressBar) {
if (this.progress > 0) {
this.progressBar.classList.add('has-progress');
} else {
this.progressBar.classList.remove('has-progress');
}
}
if (this.isAnimating) {
this.animationFrame = requestAnimationFrame(() => this.animateProgress());
}
}
setProgress(value) {
this.targetProgress = Math.min(100, Math.max(0, value));
if (!this.isAnimating && this.progressFill) {
this.startProgressAnimation();
}
}
complete() {
this.targetProgress = 100;
// Animate to 100% and then hide
const checkComplete = () => {
if (this.progress >= 99.9) {
setTimeout(() => {
this.hide();
}, 200);
} else {
setTimeout(checkComplete, 50);
}
};
checkComplete();
}
hide() {
this.isAnimating = false;
if (this.cancelProgress) {
this.cancelProgress();
}
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
}
// Clear progress elements
this.progressBar = null;
this.progressFill = null;
this.progressText = null;
if (this.container) {
this.container.innerHTML = '';
}
}
}

View File

@@ -0,0 +1,79 @@
// Module List Component
export class ModuleList {
constructor(onModuleClick) {
this.container = document.getElementById('moduleList');
this.onModuleClick = onModuleClick;
this.init();
}
async init() {
await this.loadModuleList();
}
async loadModuleList() {
try {
const response = await fetch('/modules');
const data = await response.json();
const list = document.createElement('div');
list.className = 'module-items';
// Add builtins section
if (data.builtins && data.builtins.length > 0) {
const builtinsSection = this.createSection('Builtins', data.builtins.slice(0, 20));
list.appendChild(builtinsSection);
}
// Add modules section
if (data.modules && data.modules.length > 0) {
const modulesSection = this.createSection('Standard Library', data.modules);
list.appendChild(modulesSection);
}
if (this.container) {
this.container.innerHTML = '';
this.container.appendChild(list);
}
} catch (error) {
if (this.container) {
this.container.innerHTML = `<div class="error-message">Error loading modules: ${error.message}</div>`;
}
}
}
createSection(title, items) {
const section = document.createElement('div');
section.className = 'module-section';
const sectionTitle = document.createElement('h4');
sectionTitle.textContent = title;
sectionTitle.className = 'module-section-title';
section.appendChild(sectionTitle);
const itemsList = document.createElement('div');
itemsList.className = 'module-items-list';
items.forEach(item => {
const fullName = item.full_name || (title === 'Builtins' ? `builtins.${item.name}` : item.name);
const btn = this.createModuleButton(fullName, item.name);
itemsList.appendChild(btn);
});
section.appendChild(itemsList);
return section;
}
createModuleButton(fullName, displayName) {
const btn = document.createElement('button');
btn.className = 'module-item';
btn.textContent = displayName;
btn.title = fullName;
btn.addEventListener('click', () => {
if (this.onModuleClick) {
this.onModuleClick(fullName);
}
});
return btn;
}
}

View File

@@ -0,0 +1,233 @@
// Results Component
import { LoadingProgress } from './LoadingProgress.js';
export class Results {
constructor() {
this.container = document.getElementById('results');
this.loadingProgress = new LoadingProgress(this.container);
this.init();
}
init() {
// Show welcome message initially
this.showWelcome();
}
showWelcome() {
if (this.container) {
this.container.innerHTML = `
<div class="welcome-message">
<h1>Python Documentation</h1>
<p>Search for any Python object in the sidebar to view its documentation.</p>
<p>Select a language to automatically translate the documentation.</p>
</div>
`;
}
}
showLoading(message = 'Loading documentation...', showProgress = true) {
if (this.container) {
this.loadingProgress.show(message, showProgress);
}
}
hideLoading() {
if (this.loadingProgress) {
this.loadingProgress.complete();
}
}
showError(message) {
if (this.container) {
this.container.innerHTML = `<div class="error-message">${message}</div>`;
}
}
displayDocumentation(data) {
if (!this.container) return;
const wrapper = document.createElement('div');
// Header with title
const header = document.createElement('div');
header.className = 'doc-header';
const title = document.createElement('h1');
title.className = 'doc-title';
title.textContent = data.object_name;
const meta = document.createElement('div');
meta.className = 'doc-meta';
meta.innerHTML = `
<span class="type-badge">${data.object_type || 'unknown'}</span>
${data.cached ? '<span class="doc-badge">Cached</span>' : ''}
`;
header.appendChild(title);
header.appendChild(meta);
wrapper.appendChild(header);
// Signature
if (data.signature) {
const signature = document.createElement('div');
signature.className = 'doc-signature';
signature.textContent = data.signature;
wrapper.appendChild(signature);
}
// Main documentation content
const docText = data.translated || data.original;
if (docText) {
const docSection = document.createElement('section');
docSection.className = 'doc-section';
const docTextEl = document.createElement('div');
docTextEl.className = 'doc-text';
docTextEl.textContent = docText;
docSection.appendChild(docTextEl);
wrapper.appendChild(docSection);
}
// Show original if translation exists (collapsible)
if (data.translated && data.original) {
const originalSection = document.createElement('details');
originalSection.className = 'doc-section';
originalSection.innerHTML = `
<summary style="cursor: pointer; font-weight: 500; margin-bottom: 0.5rem; color: var(--text-secondary);">
Original Documentation (English)
</summary>
<div class="doc-text" style="margin-top: 0.5rem;">${data.original}</div>
`;
wrapper.appendChild(originalSection);
}
if (!data.original && !data.translated) {
const noDoc = document.createElement('div');
noDoc.className = 'doc-text';
noDoc.textContent = 'No documentation available for this object.';
wrapper.appendChild(noDoc);
}
this.container.innerHTML = '';
this.container.appendChild(wrapper);
}
displayCourse(courseData) {
if (!this.container) return;
const wrapper = document.createElement('div');
wrapper.className = 'course-content';
const title = document.createElement('h1');
title.textContent = courseData.title || 'Python Course';
wrapper.appendChild(title);
if (courseData.sections && courseData.sections.length > 0) {
courseData.sections.forEach(section => {
const sectionDiv = this.createCourseSection(section);
wrapper.appendChild(sectionDiv);
});
}
this.container.innerHTML = '';
this.container.appendChild(wrapper);
}
createCourseSection(section) {
const sectionDiv = document.createElement('section');
sectionDiv.className = 'course-section';
sectionDiv.id = `section-${section.id || section.title.toLowerCase().replace(/\s+/g, '-')}`;
// Parse and render markdown
if (section.markdown) {
// Configure marked options
if (typeof marked !== 'undefined') {
marked.setOptions({
highlight: function(code, lang) {
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {
console.error('Highlight error:', err);
}
}
if (typeof hljs !== 'undefined') {
return hljs.highlightAuto(code).value;
}
return code;
},
breaks: true,
gfm: true
});
// Convert markdown to HTML
const htmlContent = marked.parse(section.markdown);
// Create a container for the markdown content
const contentDiv = document.createElement('div');
contentDiv.className = 'markdown-content';
contentDiv.innerHTML = htmlContent;
// Add IDs to headings for navigation
contentDiv.querySelectorAll('h1, h2, h3, h4').forEach((heading) => {
const text = heading.textContent.trim();
const id = text.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
heading.id = id;
});
// Highlight code blocks
if (typeof hljs !== 'undefined') {
contentDiv.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
sectionDiv.appendChild(contentDiv);
}
} else if (section.content && section.content.length > 0) {
// Fallback to old format
section.content.forEach(item => {
if (item.startsWith('```')) {
const codeDiv = document.createElement('pre');
codeDiv.className = 'doc-signature';
codeDiv.textContent = item.replace(/```python\n?/g, '').replace(/```/g, '').trim();
sectionDiv.appendChild(codeDiv);
} else {
const itemDiv = document.createElement('p');
itemDiv.className = 'doc-text';
itemDiv.textContent = item;
sectionDiv.appendChild(itemDiv);
}
});
}
return sectionDiv;
}
scrollToSection(section) {
const sectionId = section.id || section.title.toLowerCase().replace(/\s+/g, '-');
const sectionElement = document.getElementById(`section-${sectionId}`);
if (sectionElement) {
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
// Try to find by heading ID
const headingId = section.title.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
const headingElement = document.getElementById(headingId);
if (headingElement) {
headingElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
}

View File

@@ -0,0 +1,281 @@
// Search Bar Component with Autocomplete
export class SearchBar {
constructor(onSearch, onSelect) {
this.onSearch = onSearch;
this.onSelect = onSelect;
this.searchInput = null;
this.suggestionsContainer = null;
this.allItems = [];
this.filteredItems = [];
this.selectedIndex = -1;
this.debounceTimer = null;
this.isOpen = false;
this.init();
}
async init() {
await this.loadAllItems();
this.createSearchBar();
}
async loadAllItems() {
try {
const response = await fetch('/modules');
const data = await response.json();
this.allItems = [];
// Add builtins
if (data.builtins && data.builtins.length > 0) {
data.builtins.forEach(item => {
this.allItems.push({
name: item.name,
fullName: item.full_name || `builtins.${item.name}`,
type: 'builtin',
display: item.name
});
});
}
// Add modules
if (data.modules && data.modules.length > 0) {
data.modules.forEach(item => {
this.allItems.push({
name: item.name,
fullName: item.name,
type: 'module',
display: item.name
});
});
}
} catch (error) {
console.error('Error loading items:', error);
this.allItems = [];
}
}
createSearchBar() {
const searchForm = document.querySelector('.search-form');
if (!searchForm) return;
// Create search input
const searchWrapper = document.createElement('div');
searchWrapper.className = 'search-bar-wrapper';
const searchIcon = document.createElement('span');
searchIcon.className = 'search-icon';
searchIcon.innerHTML = '🔍';
searchIcon.setAttribute('aria-hidden', 'true');
this.searchInput = document.createElement('input');
this.searchInput.type = 'text';
this.searchInput.className = 'search-bar-input';
this.searchInput.placeholder = 'Search Python objects, modules, builtins...';
this.searchInput.autocomplete = 'off';
this.searchInput.setAttribute('aria-label', 'Search Python documentation');
// Create suggestions dropdown
this.suggestionsContainer = document.createElement('div');
this.suggestionsContainer.className = 'search-suggestions';
this.suggestionsContainer.id = 'searchSuggestions';
searchWrapper.appendChild(searchIcon);
searchWrapper.appendChild(this.searchInput);
searchWrapper.appendChild(this.suggestionsContainer);
// Replace the old input group
const oldInputGroup = searchForm.querySelector('.input-group');
if (oldInputGroup) {
oldInputGroup.replaceWith(searchWrapper);
} else {
searchForm.insertBefore(searchWrapper, searchForm.firstChild);
}
this.setupEventListeners();
}
setupEventListeners() {
// Debounced search on input
this.searchInput.addEventListener('input', (e) => {
clearTimeout(this.debounceTimer);
const query = e.target.value.trim();
if (query.length === 0) {
this.hideSuggestions();
return;
}
this.debounceTimer = setTimeout(() => {
this.filterItems(query);
this.showSuggestions();
}, 150);
});
// Handle keyboard navigation
this.searchInput.addEventListener('keydown', (e) => {
if (!this.isOpen || this.filteredItems.length === 0) {
if (e.key === 'Enter' && this.searchInput.value.trim()) {
this.performSearch(this.searchInput.value.trim());
}
return;
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredItems.length - 1);
this.updateSelection();
break;
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.updateSelection();
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0 && this.selectedIndex < this.filteredItems.length) {
this.selectItem(this.filteredItems[this.selectedIndex]);
} else if (this.searchInput.value.trim()) {
this.performSearch(this.searchInput.value.trim());
}
break;
case 'Escape':
this.hideSuggestions();
break;
}
});
// Close suggestions when clicking outside
document.addEventListener('click', (e) => {
if (!this.searchInput.contains(e.target) &&
!this.suggestionsContainer.contains(e.target)) {
this.hideSuggestions();
}
});
// Auto-search on blur if there's a value and user typed something
let hasTyped = false;
this.searchInput.addEventListener('input', () => {
hasTyped = true;
});
this.searchInput.addEventListener('blur', () => {
// Delay to allow click events on suggestions
setTimeout(() => {
const query = this.searchInput.value.trim();
if (query && hasTyped && !this.isOpen) {
this.performSearch(query);
}
hasTyped = false;
}, 200);
});
}
filterItems(query) {
const lowerQuery = query.toLowerCase();
this.filteredItems = this.allItems
.filter(item => {
const nameMatch = item.name.toLowerCase().includes(lowerQuery);
const fullNameMatch = item.fullName.toLowerCase().includes(lowerQuery);
return nameMatch || fullNameMatch;
})
.slice(0, 10); // Limit to 10 suggestions
this.selectedIndex = -1;
}
showSuggestions() {
if (this.filteredItems.length === 0) {
this.hideSuggestions();
return;
}
this.isOpen = true;
this.suggestionsContainer.innerHTML = '';
this.suggestionsContainer.classList.add('active');
this.filteredItems.forEach((item, index) => {
const suggestion = document.createElement('div');
suggestion.className = 'search-suggestion';
suggestion.dataset.index = index;
const icon = item.type === 'builtin' ? '⚡' : '📦';
suggestion.innerHTML = `
<span class="suggestion-icon">${icon}</span>
<span class="suggestion-name">${this.highlightMatch(item.name, this.searchInput.value)}</span>
<span class="suggestion-type">${item.type}</span>
`;
suggestion.addEventListener('click', () => {
this.selectItem(item);
});
suggestion.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateSelection();
});
this.suggestionsContainer.appendChild(suggestion);
});
}
highlightMatch(text, query) {
if (!query) return text;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const index = lowerText.indexOf(lowerQuery);
if (index === -1) return text;
const before = text.substring(0, index);
const match = text.substring(index, index + query.length);
const after = text.substring(index + query.length);
return `${before}<mark>${match}</mark>${after}`;
}
updateSelection() {
const suggestions = this.suggestionsContainer.querySelectorAll('.search-suggestion');
suggestions.forEach((suggestion, index) => {
if (index === this.selectedIndex) {
suggestion.classList.add('selected');
suggestion.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
} else {
suggestion.classList.remove('selected');
}
});
}
selectItem(item) {
this.searchInput.value = item.fullName;
this.hideSuggestions();
if (this.onSelect) {
this.onSelect(item.fullName);
}
this.performSearch(item.fullName);
}
performSearch(query) {
if (!query || query.trim().length === 0) return;
if (this.onSearch) {
this.onSearch(query.trim());
}
}
hideSuggestions() {
this.isOpen = false;
this.selectedIndex = -1;
this.suggestionsContainer.classList.remove('active');
this.suggestionsContainer.innerHTML = '';
}
setValue(value) {
if (this.searchInput) {
this.searchInput.value = value;
}
}
}

View File

@@ -0,0 +1,8 @@
// Search Form Component (simplified - now just a container)
export class SearchForm {
constructor() {
// This component is now just a container
// Actual search is handled by SearchBar component
}
}

View File

@@ -0,0 +1,123 @@
// Sidebar Component
export class Sidebar {
constructor() {
this.sidebar = document.getElementById('sidebar');
this.resizeHandle = document.getElementById('resizeHandle');
this.menuToggle = null;
this.isResizing = false;
this.startX = 0;
this.startWidth = 0;
this.init();
}
init() {
this.createMenuToggle();
this.setupResize();
this.setupMobileBehavior();
}
createMenuToggle() {
// Create mobile menu toggle button
this.menuToggle = document.createElement('button');
this.menuToggle.className = 'mobile-menu-toggle';
this.menuToggle.innerHTML = '☰';
this.menuToggle.setAttribute('aria-label', 'Toggle menu');
this.menuToggle.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleMobile();
});
// Create overlay for mobile
this.overlay = document.createElement('div');
this.overlay.className = 'sidebar-overlay';
this.overlay.addEventListener('click', () => this.closeMobile());
// Insert at the beginning of body
document.body.insertBefore(this.menuToggle, document.body.firstChild);
document.body.appendChild(this.overlay);
}
setupResize() {
if (!this.resizeHandle || !this.sidebar) return;
this.resizeHandle.addEventListener('mousedown', (e) => {
if (window.innerWidth <= 768) return; // Disable resize on mobile
this.isResizing = true;
this.startX = e.clientX;
this.startWidth = parseInt(window.getComputedStyle(this.sidebar).width, 10);
document.addEventListener('mousemove', this.handleResize.bind(this));
document.addEventListener('mouseup', this.stopResize.bind(this));
e.preventDefault();
});
}
handleResize(e) {
if (!this.isResizing) return;
const width = this.startWidth + e.clientX - this.startX;
const minWidth = 200;
const maxWidth = 600;
if (width >= minWidth && width <= maxWidth) {
this.sidebar.style.width = `${width}px`;
document.documentElement.style.setProperty('--sidebar-width', `${width}px`);
}
}
stopResize() {
this.isResizing = false;
document.removeEventListener('mousemove', this.handleResize);
document.removeEventListener('mouseup', this.stopResize);
}
setupMobileBehavior() {
// Close sidebar on window resize if switching to desktop
window.addEventListener('resize', () => {
if (window.innerWidth > 768) {
this.closeMobile();
}
});
// Close sidebar when clicking on module items or search button on mobile
if (this.sidebar) {
this.sidebar.addEventListener('click', (e) => {
if (window.innerWidth <= 768) {
// Close if clicking on interactive elements (but not the toggle itself)
if (e.target.closest('.module-item') ||
e.target.closest('.search-btn') ||
e.target.closest('.nav-tab')) {
// Small delay to allow the click to register
setTimeout(() => this.closeMobile(), 100);
}
}
});
}
}
toggleMobile() {
const isOpen = this.sidebar?.classList.contains('open');
if (isOpen) {
this.closeMobile();
} else {
this.openMobile();
}
}
openMobile() {
this.sidebar?.classList.add('open');
if (this.overlay) {
this.overlay.classList.add('active');
}
// Prevent body scroll when sidebar is open
document.body.style.overflow = 'hidden';
}
closeMobile() {
this.sidebar?.classList.remove('open');
if (this.overlay) {
this.overlay.classList.remove('active');
}
// Restore body scroll
document.body.style.overflow = '';
}
}

54
static/components/Tabs.js Normal file
View File

@@ -0,0 +1,54 @@
// Tabs Component
export class Tabs {
constructor(onTabChange) {
this.navTabs = document.querySelectorAll('.nav-tab');
this.docsTab = document.getElementById('docsTab');
this.courseTab = document.getElementById('courseTab');
this.onTabChange = onTabChange;
this.init();
}
init() {
this.navTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabName = tab.dataset.tab;
this.switchTab(tabName);
});
});
}
switchTab(tabName) {
// Update active tab
this.navTabs.forEach(t => t.classList.remove('active'));
const activeTab = Array.from(this.navTabs).find(t => t.dataset.tab === tabName);
if (activeTab) {
activeTab.classList.add('active');
}
// Show/hide tab content
if (tabName === 'docs') {
if (this.docsTab) {
this.docsTab.style.display = 'flex';
this.docsTab.classList.add('active');
}
if (this.courseTab) {
this.courseTab.style.display = 'none';
this.courseTab.classList.remove('active');
}
} else {
if (this.docsTab) {
this.docsTab.style.display = 'none';
this.docsTab.classList.remove('active');
}
if (this.courseTab) {
this.courseTab.style.display = 'flex';
this.courseTab.classList.add('active');
}
if (this.onTabChange) {
this.onTabChange(tabName);
}
}
}
}

View File

@@ -0,0 +1,26 @@
// Theme Toggle Component
export class ThemeToggle {
constructor() {
this.html = document.documentElement;
this.toggle = document.getElementById('themeToggle');
this.init();
}
init() {
// Load saved theme
const savedTheme = localStorage.getItem('theme') || 'light';
this.html.setAttribute('data-theme', savedTheme);
if (this.toggle) {
this.toggle.addEventListener('click', () => this.toggleTheme());
}
}
toggleTheme() {
const currentTheme = this.html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
this.html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
}