wiki/docs/assets/extra.css
2025-08-06 13:00:32 +01:00

420 lines
12 KiB
CSS
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

.meganote { font-style: italic; } /* example utility class */
#wiki-multipane {
display: grid;
grid-template-columns: minmax(25rem, 1fr) minmax(20rem, 1fr); /* main (min 600px) | source (400px) */
grid-template-rows: 1fr;
gap: 1rem;
}
.megapage-grid {
/* Remove grid properties - let Material handle its normal layout */
display: block;
}
/* Source pane positioning - fixed relative to viewport but below header */
#wiki-source-pane {
border-left: 1px solid var(--md-default-fg-color--lightest);
border-radius: 0.5rem 0 0 0.5rem;
position: fixed;
top: 2.5rem; /* Below Material's header */
right: 0rem;
width: calc(40% - 1.5rem); /* Responsive width minus gap */
min-width: 20rem;
height: calc(100vh - 2.5rem); /* Full height minus header and small margin */
padding: 0;
background: var(--md-default-bg-color);
z-index: 100;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Source pane header with controls */
.source-pane-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0rem;
border-bottom: 1px solid var(--md-default-fg-color--lightest);
background: var(--md-default-bg-color);
flex-shrink: 0;
position: absolute;
width: 100%;
min-width: 20rem;
z-index: 1000;
opacity: 1.0;
}
.source-pane-links {
display: flex;
gap: 0;
align-items: center;
}
.source-pane-controls {
display: flex;
gap: 0.25rem;
align-items: center;
}
.source-control-btn {
background: var(--md-default-accent-fg-color);
color: var(--md-default-fg-color);
/* border: 1px solid var(--md-default-fg-color--light); */
font-size: 12px;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
transition: background-color 0.2s;
}
.source-control-btn:hover {
background: var(--md-default-fg-color--lightest);
text-decoration: none;
}
.source-control-btn .icon {
font-size: 10px;
}
#source-frame {
width: 100%;
height: 100%;
flex: 1;
border: none;
border-radius: 0;
}
/* Adjust main content margin when source pane is visible */
#wiki-multipane:has(#wiki-source-pane[style*="block"]) #wiki-main-pane {
margin-right: 1rem;
}
/* Close button styling */
.close-btn {
background: var(--md-accent-fg-color);
color: white;
border: none;
font-size: 16px;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
line-height: 1;
font-weight: bold;
}
.close-btn:hover {
opacity: 0.8;
}
/* Hide the source pane when there's not enough space for both main (600px) + source (400px) + gap */
@media (max-width: 64rem) { /* 600px + 400px + gap ≈ 1024px */
#wiki-multipane { grid-template-columns: 1fr; }
#wiki-source-pane { display: none !important; }
#wiki-main-pane { margin-right: 0 !important; }
}
/* Dark mode adjustments */
@media (prefers-color-scheme: dark) {
#wiki-source-pane {
border-left-color: var(--md-default-fg-color--lightest);
}
}
.md-footer {
display: none;
}ove source parameter from hash
const currentHash = window.location.hash;
const newHash = currentHash
.replace(/[#&]source=[^&]*/, '')
.replace(/^&/, '#')
.replace(/&$/, '');
if (newHash !== currentHash) {
history.pushState(null, '', newHash || '#');
}
}
async function discoverAndPreloadSources() {
console.log('🔍 Discovering available source pages...');
// Find all source-link elements on the page
const sourceLinks = document.querySelectorAll('a.source-link');
const discoveryPromises = [];
for (const link of sourceLinks) {
const originalUrl = link.href;
const localPath = `/sources/${await slug(originalUrl)}`;
// Check if local version exists (do this once during preload)
discoveryPromises.push(
fetch(localPath, { method: 'HEAD' })
.then(response => {
if (response.ok) {
sourceCache.registerSource(originalUrl, localPath);
console.log(`📍 Registered: ${originalUrl} -> ${localPath}`);
}
})
.catch(() => {
// Local version doesn't exist, that's fine
})
);
}
await Promise.all(discoveryPromises);
console.log(` Discovery complete. Found ${sourceCache.sourceRegistry.size} local sources.`);
}
async function preloadSource(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const content = await response.text();
sourceCache.set(url, content);
return content;
} catch (error) {
console.warn(`Failed to preload source: ${url}`, error);
return null;
}
}
async function preloadAllSources() {
if (sourceCache.preloadComplete) return;
await discoverAndPreloadSources();
// Preload a few of the most recently discovered sources
const localSources = Array.from(sourceCache.sourceRegistry.values()).slice(0, 10); // Limit to first 10
if (localSources.length > 0) {
console.log(`📦 Preloading ${localSources.length} source pages...`);
const preloadPromises = localSources.map(async (localPath) => {
if (!sourceCache.has(localPath)) {
await preloadSource(localPath);
}
});
await Promise.all(preloadPromises);
console.log(`🚀 Preloaded ${sourceCache.size()} source pages`);
}
sourceCache.preloadComplete = true;
}
function createDataUrl(htmlContent) {
// Create a data URL for the HTML content
const blob = new Blob([htmlContent], { type: 'text/html' });
return URL.createObjectURL(blob);
}
function updateSourceControls() {
const originalBtn = document.querySelector('.source-control-original');
const archiveBtn = document.querySelector('.source-control-archive');
if (!currentSourceUrl || !originalBtn || !archiveBtn) return;
// Update original button
originalBtn.href = currentSourceUrl;
// Update archive button
if (sourceCache.isAvailableLocally(currentSourceUrl)) {
const localPath = sourceCache.getLocalPath(currentSourceUrl);
archiveBtn.href = localPath;
archiveBtn.style.display = 'inline-flex';
} else {
archiveBtn.style.display = 'none';
}
}
async function showSourcePane(sourceUrl) {
const pane = document.getElementById('wiki-source-pane');
const frame = document.getElementById('source-frame');
if (!hasSpaceForSourcePane()) {
window.open(sourceUrl, '_blank');
return;
}
// Update current source URL for header controls
currentSourceUrl = sourceUrl;
// Show pane first for immediate feedback
if (pane) pane.style.display = 'block';
// Update header controls
updateSourceControls();
// Determine target without any network requests
let target = sourceUrl;
if (sourceCache.isAvailableLocally(sourceUrl)) {
target = sourceCache.getLocalPath(sourceUrl);
}
// Check if we have cached content (instant load)
if (sourceCache.has(target)) {
const cachedContent = sourceCache.get(target);
const dataUrl = createDataUrl(cachedContent);
if (frame) frame.src = dataUrl;
console.log(` Instant load from cache: ${target}`);
} else {
// Only make network request if not cached yet
if (target.startsWith('/sources/')) {
console.log(`📥 Loading and caching: ${target}`);
const content = await preloadSource(target);
if (content && frame) {
const dataUrl = createDataUrl(content);
frame.src = dataUrl;
} else if (frame) {
frame.src = target; // Fallback
}
} else {
// External URL - load directly
if (frame) frame.src = target;
}
}
// Update URL
setSourceUrlInHash(sourceUrl);
}
function hideSourcePane() {
const pane = document.getElementById('wiki-source-pane');
if (pane) {
pane.style.display = 'none';
currentSourceUrl = null;
}
removeSourceFromHash();
}
function createSourcePaneHeader() {
const header = document.createElement('div');
header.className = 'source-pane-header';
const title = document.createElement('span');
title.textContent = 'Source';
title.style.fontWeight = 'bold';
title.style.fontSize = '14px';
const controls = document.createElement('div');
controls.className = 'source-pane-controls';
// Original button
const originalBtn = document.createElement('a');
originalBtn.className = 'source-control-btn source-control-original';
originalBtn.target = '_blank';
originalBtn.innerHTML = '<span class="icon"></span>original';
// Archive button
const archiveBtn = document.createElement('a');
archiveBtn.className = 'source-control-btn source-control-archive';
archiveBtn.target = '_blank';
archiveBtn.innerHTML = '<span class="icon"></span>archive';
// Close button
const closeBtn = document.createElement('button');
closeBtn.className = 'close-btn';
closeBtn.innerHTML = '×';
closeBtn.addEventListener('click', hideSourcePane);
controls.appendChild(originalBtn);
controls.appendChild(archiveBtn);
controls.appendChild(closeBtn);
header.appendChild(title);
header.appendChild(controls);
return header;
}
/* ---------- main ----------------------------------------------------- */
document.addEventListener('DOMContentLoaded', async () => {
const pane = document.getElementById('wiki-source-pane'); // wrapper <aside>
const frame = document.getElementById('source-frame'); // iframe inside it
if (pane) pane.style.display = 'none'; // start hidden
// Add header with controls to source pane
if (pane && !pane.querySelector('.source-pane-header')) {
const header = createSourcePaneHeader();
pane.insertBefore(header, frame);
}
// Preload sources in the background
setTimeout(() => {
preloadAllSources().catch(console.error);
}, 1000); // Wait 1 second after page load
// Check URL hash on page load
const initialSourceUrl = getSourceUrlFromHash();
if (initialSourceUrl && hasSpaceForSourcePane()) {
showSourcePane(initialSourceUrl);
}
document.body.addEventListener('click', async ev => {
const link = ev.target.closest('a.source-link');
if (!link) return;
ev.preventDefault();
await showSourcePane(link.href);
});
/* Handle window resize - hide source pane if not enough space */
window.addEventListener('resize', () => {
if (pane && !hasSpaceForSourcePane()) {
hideSourcePane();
}
});
/* Handle browser back/forward buttons */
window.addEventListener('popstate', () => {
const sourceUrl = getSourceUrlFromHash();
if (sourceUrl && hasSpaceForSourcePane()) {
showSourcePane(sourceUrl);
} else {
hideSourcePane();
}
});
// Add cache management to window for debugging
if (typeof window !== 'undefined') {
window.sourceCache = sourceCache;
window.clearSourceCache = () => {
sourceCache.clear();
console.log('🗑 Source cache cleared');
};
window.preloadAllSources = preloadAllSources;
}
});
```
Key features of the new header:
1. **Header Bar**: Added a clean header with title "Source" and controls on the right
2. **Original Button**: ` original` - Opens the original URL in a new tab
3. **Archive Button**: ` archive` - Opens the local archived version in a new tab (only shows if archive exists)
4. **Close Button**: `×` - Closes the source pane (red accent color)
5. **Dynamic Updates**: Buttons update their links when you switch between different sources
6. **Responsive Design**: Header adapts to the pane width and theme colors
7. **Flexbox Layout**: Source pane now uses flexbox with header at top and iframe filling remaining space
The header will show:
- `[] original [] archive ... [×]` when an archive exists locally
- `[] original ... [×]` when only the original URL is available
The styling matches Material's design system and works in both light and dark modes!