Wave changes

This commit is contained in:
2026-03-27 15:39:35 +00:00
parent cb3e4c5809
commit bddb928ec2
253 changed files with 7056 additions and 1727 deletions

0
assets/2_to_1.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

0
assets/Big-O-Notation-3130482830.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 232 KiB

0
assets/Screenshot_20251227_153037.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

0
assets/career-notes/api-layers.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

0
assets/career/APIOps.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

0
assets/career/Screenshot 2026-01-14 154800.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

0
assets/design-patterns/adapter-pattern-2.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

0
assets/design-patterns/adapter-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

0
assets/design-patterns/command-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

0
assets/design-patterns/composite-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

0
assets/design-patterns/decorator-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

0
assets/design-patterns/decorator-problem.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

0
assets/design-patterns/facade-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

0
assets/design-patterns/factory-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

0
assets/design-patterns/iterator-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

0
assets/design-patterns/observer-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

0
assets/design-patterns/proxy-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

0
assets/design-patterns/singleton-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

0
assets/design-patterns/state-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

0
assets/design-patterns/structure-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

0
assets/design-patterns/template-method-pattern.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

0
assets/git-cheat-sheet-1.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 485 KiB

After

Width:  |  Height:  |  Size: 485 KiB

0
assets/git-cheat-sheet-2.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 347 KiB

0
assets/gr.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

0
assets/icons/icons8-z-key-64.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

0
assets/scripts/bigger-picture.min.js vendored Executable file → Normal file
View File

View File

@@ -1,7 +1,12 @@
/**
* cookbook.js
* Works both as a direct page load AND when injected as a stacked pane
* by script.js (which fetches pages and injects only #content).
* cookbook.js — fixed for the new tab-based layout
*
* Changes vs original:
* 1. initCookbook() is exported so script.js can call it after tab content loads
* 2. cellValue() scans ALL tr elements (not just tbody) — org-mode hides thead via CSS
* but the rows are still in the DOM under thead
* 3. watchForPanes() now watches #content-area (the new tab container) instead of .stack-track
* 4. Toolbar falls back to inserting before the first .outline-3 if .outline-2 is absent
*/
(function () {
@@ -10,7 +15,6 @@
// ── Core init — scoped to a content root ──────────────────────────────────
function initCookbook(root) {
// Already initialised on this root?
if (root.dataset.cbInit) return;
root.dataset.cbInit = "1";
@@ -22,7 +26,9 @@
// ── Helpers ─────────────────────────────────────────────────────────────
function cellValue(table, label) {
for (const row of table.querySelectorAll("tbody tr")) {
// Scan ALL tr elements — org-mode thead is hidden via CSS but rows are
// still in the DOM there; relying only on tbody misses them.
for (const row of table.querySelectorAll("tr")) {
const cells = row.querySelectorAll("td");
if (cells.length >= 2 && cells[0].textContent.trim() === label)
return cells[1].textContent.trim();
@@ -61,8 +67,17 @@
<span class="cb-count" aria-live="polite"></span>
</div>`;
const firstSection = root.querySelector(".outline-2");
if (firstSection) firstSection.parentNode.insertBefore(toolbar, firstSection);
// Insert before first .outline-2; fall back to first .outline-3
const insertTarget =
root.querySelector(".outline-2") ||
root.querySelector(".outline-3");
if (insertTarget) {
insertTarget.parentNode.insertBefore(toolbar, insertTarget);
} else {
// Last resort: prepend into root
root.prepend(toolbar);
}
const searchInput = toolbar.querySelector(".cb-search");
const filterBtns = toolbar.querySelectorAll(".cb-filter-btn");
@@ -95,7 +110,7 @@
const cooked = isCooked(table);
const title = (block.querySelector("h3")?.textContent ?? "").toLowerCase();
const kwRaw = cellValue(table, "Keywords");
const kwLow = kwRaw.toLowerCase();
const kwLow = kwLow = kwRaw.toLowerCase();
if (activeFilter === "cooked" && !cooked) { hide(block); return; }
if (activeFilter === "uncooked" && cooked) { hide(block); return; }
@@ -113,7 +128,6 @@
shown++;
});
// Hide category headings with no visible recipes
root.querySelectorAll(".outline-2").forEach(section => {
const hasVisible = Array.from(section.querySelectorAll(".outline-3"))
.some(b => b.style.display !== "none");
@@ -184,7 +198,6 @@
// ── Entry points ──────────────────────────────────────────────────────────
// 1. Direct page load — #content is already in the DOM
function tryInitOnContent() {
const content = document.getElementById("content");
if (content && content.querySelector("table.recipe-meta")) {
@@ -192,39 +205,57 @@
}
}
// 2. Stacked pane — script.js fetches a page and appends a new <article>
// with the #content inside it. Watch for that.
function watchForPanes() {
const track = document.querySelector(".stack-track");
if (!track) return;
// Watch #content-area — this is where script.js appends new .tab-content divs.
// Falls back to watching document.body if #content-area isn't ready yet.
const getTrack = () =>
document.getElementById("content-area") || document.body;
const observer = new MutationObserver(mutations => {
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
// script.js appends an <article class="stack-pane">
// containing a div#content
const content = node.id === "content"
? node
: node.querySelector("#content, .content");
if (content && content.querySelector("table.recipe-meta")) {
initCookbook(content);
}
const observe = (track) => {
const observer = new MutationObserver(mutations => {
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
// script.js appends .tab-content divs containing a div#content
const content =
(node.id === "content" ? node : null) ||
node.querySelector("#content, .content");
if (content && content.querySelector("table.recipe-meta")) {
initCookbook(content);
}
});
});
});
});
observer.observe(track, { childList: true, subtree: true });
};
observer.observe(track, { childList: true, subtree: true });
const track = document.getElementById("content-area");
if (track) {
observe(track);
} else {
// content-area is built by script.js after DOMContentLoaded;
// wait for it to appear then start observing.
const bodyObserver = new MutationObserver(() => {
const area = document.getElementById("content-area");
if (area) {
bodyObserver.disconnect();
observe(area);
}
});
bodyObserver.observe(document.body, { childList: true, subtree: false });
}
}
// Run both
// Expose for direct calls from script.js if needed:
// window.initCookbook(contentEl);
window.initCookbook = initCookbook;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
tryInitOnContent();
watchForPanes();
});
} else {
// Already parsed (script has `defer` but DOM may already be ready)
tryInitOnContent();
watchForPanes();
}

291
assets/scripts/gallery-init.js Executable file → Normal file
View File

@@ -4,18 +4,33 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
// --- Shared helpers for video detection + mime typing ---
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'm4v', 'ogv'];
function isVideo(href) {
if (!href) return false;
const clean = String(href).toLowerCase().split('#')[0].split('?')[0];
return VIDEO_EXTENSIONS.some(ext => clean.endsWith('.' + ext));
}
function mimeFromExt(href) {
const ext = String(href).toLowerCase().split('#')[0].split('?')[0].split('.').pop();
switch (ext) {
case 'mp4': case 'm4v': return 'video/mp4';
case 'webm': return 'video/webm';
case 'ogv': return 'video/ogg';
case 'mov': return 'video/quicktime';
default: return 'video/*';
}
}
// -------------------------------------------------------
function setupGalleries(root = document) {
const imgs = root.querySelectorAll('.figure img, img.org-svg');
const VIDEO_EXTENSIONS = ['mp4','webm','mov','m4v','ogv'];
function isVideo(href){
if(!href) return false;
href = href.toLowerCase();
return VIDEO_EXTENSIONS.some(ext => href.endsWith('.' + ext));
}
imgs.forEach((img) => {
if (img.closest('a')) return;
const a = document.createElement('a');
const href = img.currentSrc || img.src;
a.href = href;
@@ -23,10 +38,9 @@ document.addEventListener('DOMContentLoaded', () => {
a.dataset.alt = img.alt || '';
const setDims = () => {
a.dataset.width = img.naturalWidth || img.width || 1920;
a.dataset.height = img.naturalHeight || img.height || 1080;
a.dataset.width = img.naturalWidth || img.width || 1920;
a.dataset.height = img.naturalHeight || img.height || 1080;
};
if (img.complete) setDims();
else img.addEventListener('load', setDims);
@@ -36,163 +50,153 @@ document.addEventListener('DOMContentLoaded', () => {
});
const containers = root.querySelectorAll('main, article, .content, body');
containers.forEach((container) => {
//const links = Array.from(container.querySelectorAll('.figure a, a:has(img.org-svg)'));
const links = Array.from(container.querySelectorAll(
'.figure a, a:has(img.org-svg), a[href$=".mp4"], a[href$=".webm"], a[href$=".mov"], a[href$=".m4v"], a[href$=".ogv"]'
'.figure a, a:has(img.org-svg), a[href$=".mp4"], a[href$=".webm"], a[href$=".mov"], a[href$=".m4v"], a[href$=".ogv"]'
));
if (!links.length) return;
links.forEach((link) => {
if (link.dataset.bpBound) return;
link.dataset.bpBound = 'true';
link.addEventListener('click', (e) => {
if (link.dataset.bpBound) return;
link.dataset.bpBound = 'true';
link.addEventListener('click', (e) => {
e.preventDefault();
bp.open({
items: links,
el: link,
caption: (el) => el.querySelector('img')?.alt || el.title || '',
maxZoom: 40,
onOpen(containerEl){
const linkEl = bp.currItem;
const href = linkEl?.href || "";
if (isVideo(href)) {
teardownRotation();
const htmlLayer = containerEl.querySelector(".bp-html");
const imgLayer = containerEl.querySelector(".bp-img");
if (imgLayer) imgLayer.style.display = "none";
htmlLayer.innerHTML = `
<video controls autoplay style="max-width:95vw; max-height:95vh">
<source src="${href}" type="video/mp4">
</video>
`;
return;
}
setupRotation(containerEl);
enhanceSVG(containerEl);
}
onOpen(containerEl) {
const linkEl = bp.currItem;
const href = linkEl?.href || '';
if (isVideo(href)) {
teardownRotation();
const htmlLayer = containerEl.querySelector('.bp-html');
const imgLayer = containerEl.querySelector('.bp-img');
if (imgLayer) imgLayer.style.display = 'none';
htmlLayer.innerHTML = `
<video controls autoplay style="max-width:95vw; max-height:95vh">
<source src="${href}" type="${mimeFromExt(href)}">
</video>`;
return;
}
setupRotation(containerEl);
enhanceSVG(containerEl);
}
});
});
});
});
});
}
function setupVideo(root=document){
const links = root.querySelectorAll('a[href]');
links.forEach(link=>{
if(link.dataset.videoProcessed) return;
// Replaces plain video links with inline <video> players
function setupVideo(root = document) {
// Links that point directly to video files
root.querySelectorAll('a[href]').forEach(link => {
if (link.dataset.videoProcessed) return;
const href = link.getAttribute('href');
if (!isVideo(href)) return;
link.dataset.videoProcessed = 'true';
const href = link.getAttribute("href");
if(!isVideo(href)) return;
const wrapper = document.createElement('div');
wrapper.className = 'video-container';
link.dataset.videoProcessed = "true";
const video = document.createElement('video');
video.controls = true;
video.preload = 'metadata';
video.style.cssText = 'max-width:100%;width:100%;height:auto;display:block;border-radius:6px;';
const video = document.createElement("video");
video.controls = true;
video.preload = "metadata";
video.style.maxWidth = "100%";
video.style.borderRadius = "6px";
// For .mov files: browsers (Chrome/Firefox) don't support QuickTime.
// Emit an MP4 source first (the converted sibling), then the original
// MOV as a last-resort fallback (Safari can play it).
const isMov = href.toLowerCase().split('?')[0].endsWith('.mov');
if (isMov) {
const mp4Href = href.replace(/\.mov$/i, '.mp4');
const mp4Source = document.createElement('source');
mp4Source.src = mp4Href;
mp4Source.type = 'video/mp4';
video.appendChild(mp4Source);
}
const source = document.createElement("source");
source.src = href;
source.type = "video/" + href.split('.').pop();
const source = document.createElement('source');
source.src = href;
source.type = mimeFromExt(href);
video.appendChild(source);
wrapper.appendChild(video);
video.appendChild(source);
link.parentNode.replaceChild(wrapper, link);
});
link.parentNode.replaceChild(video, link);
});
}
// Raw <video> elements not yet wrapped
root.querySelectorAll('video').forEach(video => {
video.controls = true;
video.style.cssText = 'max-width:100%;width:100%;height:auto;display:block;border-radius:6px;';
if (!video.closest('.video-container')) {
const wrapper = document.createElement('div');
wrapper.className = 'video-container';
video.parentNode.insertBefore(wrapper, video);
wrapper.appendChild(video);
}
if (!video.currentSrc) video.load();
});
}
// Replaces plain mp3 links with inline <audio> players
function setupAudio(root = document) {
const audioLinks = root.querySelectorAll('a[href$=".mp3"]');
audioLinks.forEach((link) => {
root.querySelectorAll('a[href$=".mp3"], a[href$=".ogg"], a[href$=".flac"], a[href$=".wav"]').forEach(link => {
if (link.dataset.audioProcessed) return;
link.dataset.audioProcessed = "true";
link.dataset.audioProcessed = 'true';
const src = link.href;
const wrapper = document.createElement("div");
wrapper.className = "audio-player";
const wrapper = document.createElement('div');
wrapper.className = 'audio-player';
const audio = document.createElement("audio");
const audio = document.createElement('audio');
audio.controls = true;
audio.preload = "metadata";
audio.src = src;
audio.preload = 'metadata';
audio.src = src;
const label = document.createElement("div");
label.className = "audio-label";
label.textContent = link.textContent || src.split("/").pop();
const label = document.createElement('div');
label.className = 'audio-label';
label.textContent = link.textContent || src.split('/').pop();
wrapper.appendChild(label);
wrapper.appendChild(audio);
link.replaceWith(wrapper);
});
}
// ----- BiggerPicture instance -----
const bp = BiggerPicture({ target: document.body });
// ----- Rotation helpers -----
let activeContainer = null;
let currentRotation = 0;
let rotateControls = null;
let rotateControls = null;
function ensureRotateControls() {
if (rotateControls) return rotateControls;
const wrapper = document.createElement('div');
wrapper.className = 'bp-rotate-controls';
Object.assign(wrapper.style, {
position: 'fixed',
bottom: '1.5rem',
right: '1.5rem',
display: 'flex',
gap: '0.5rem',
zIndex: '9999',
pointerEvents: 'auto'
position: 'fixed', bottom: '1.5rem', right: '1.5rem',
display: 'flex', gap: '0.5rem', zIndex: '9999', pointerEvents: 'auto'
});
const mkBtn = (label, title) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = label;
btn.title = title;
btn.type = 'button'; btn.textContent = label; btn.title = title;
btn.setAttribute('aria-label', title);
Object.assign(btn.style, {
padding: '0.4rem 0.6rem',
borderRadius: '999px',
border: 'none',
fontSize: '1.2rem',
cursor: 'pointer',
background: 'rgba(30,30,30,0.8)',
color: '#fff'
padding: '0.4rem 0.6rem', borderRadius: '999px', border: 'none',
fontSize: '1.2rem', cursor: 'pointer',
background: 'rgba(30,30,30,0.8)', color: '#fff'
});
return btn;
};
const left = mkBtn('⟲', 'Rotate image 90° left');
const left = mkBtn('⟲', 'Rotate image 90° left');
const right = mkBtn('⟳', 'Rotate image 90° right');
left.addEventListener('click', (e) => {
e.stopPropagation();
if (!activeContainer) return;
currentRotation = (currentRotation - 90 + 360) % 360;
applyRotation();
});
right.addEventListener('click', (e) => {
e.stopPropagation();
if (!activeContainer) return;
currentRotation = (currentRotation + 90) % 360;
applyRotation();
});
left.addEventListener('click', e => { e.stopPropagation(); if (activeContainer) { currentRotation = (currentRotation - 90 + 360) % 360; applyRotation(); } });
right.addEventListener('click', e => { e.stopPropagation(); if (activeContainer) { currentRotation = (currentRotation + 90) % 360; applyRotation(); } });
wrapper.append(left, right);
document.body.appendChild(wrapper);
rotateControls = wrapper;
@@ -201,44 +205,36 @@ function setupVideo(root=document){
function ensureRotateWrapper() {
if (!activeContainer) return null;
const imgRoot = activeContainer.querySelector('.bp-img');
if (!imgRoot) return null;
const imgEl = imgRoot.querySelector('img');
if (!imgEl) return null;
const src = (imgEl.currentSrc || imgEl.src || '').toLowerCase();
if (src.endsWith('.svg')) return null;
let wrapper = imgRoot.querySelector('.bp-rotate-wrapper');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.className = 'bp-rotate-wrapper';
wrapper.style.display = 'inline-block';
wrapper.style.transformOrigin = 'center center';
imgRoot.appendChild(wrapper);
wrapper.appendChild(imgEl);
} else if (!wrapper.contains(imgEl)) {
wrapper.innerHTML = '';
wrapper.appendChild(imgEl);
}
return wrapper;
}
function applyRotation() {
const wrapper = ensureRotateWrapper();
if (!wrapper) return;
wrapper.style.transform = `rotate(${currentRotation}deg)`;
if (wrapper) wrapper.style.transform = `rotate(${currentRotation}deg)`;
}
function setupRotation(containerEl) {
activeContainer = containerEl;
currentRotation = 0;
const controls = ensureRotateControls();
controls.style.display = 'flex';
ensureRotateControls().style.display = 'flex';
applyRotation();
}
@@ -248,17 +244,16 @@ function setupVideo(root=document){
if (rotateControls) rotateControls.style.display = 'none';
}
// ----- SVG pan/zoom -----
let activePanZoom = null;
const destroyPanZoom = () => { try { activePanZoom?.destroy(); } catch(_){} activePanZoom = null; };
async function enhanceSVG(containerEl) {
try {
destroyPanZoom();
const imgEl = containerEl.querySelector('.bp-img img');
if (!imgEl) return;
const src = imgEl.currentSrc || imgEl.src || '';
const src = imgEl.currentSrc || imgEl.src || '';
const isSVG = src.toLowerCase().endsWith('.svg');
const htmlLayer = containerEl.querySelector('.bp-html');
if (!isSVG || !htmlLayer) {
@@ -267,42 +262,32 @@ function setupVideo(root=document){
imgEl.style.visibility = '';
return;
}
let holder = htmlLayer.querySelector('.bp-svg-holder');
if (!holder) {
holder = document.createElement('div');
holder.className = 'bp-svg-holder';
holder.style.maxWidth = '95vw';
holder.style.maxWidth = '95vw';
holder.style.maxHeight = '95vh';
htmlLayer.appendChild(holder);
}
holder.innerHTML = '';
imgEl.style.visibility = 'hidden';
const res = await fetch(src, { cache: 'force-cache' });
const res = await fetch(src, { cache: 'force-cache' });
const text = await res.text();
holder.innerHTML = text;
const svg = holder.querySelector('svg');
if (!svg) { imgEl.style.visibility = ''; return; }
svg.style.maxWidth = '95vw';
svg.style.maxWidth = '95vw';
svg.style.maxHeight = '95vh';
svg.style.display = 'block';
svg.style.display = 'block';
if (typeof window.svgPanZoom === 'function') {
activePanZoom = svgPanZoom(svg, {
zoomEnabled: true,
controlIconsEnabled: true,
fit: true,
center: true,
minZoom: 0.05,
maxZoom: 400,
zoomScaleSensitivity: 0.25,
zoomEnabled: true, controlIconsEnabled: true,
fit: true, center: true,
minZoom: 0.05, maxZoom: 400, zoomScaleSensitivity: 0.25,
dblClickZoomEnabled: true
});
holder.addEventListener('wheel', (e) => e.stopPropagation(), { passive: true });
holder.addEventListener('wheel', e => e.stopPropagation(), { passive: true });
} else {
console.warn('[gallery-init] svg-pan-zoom not loaded');
}
@@ -311,33 +296,41 @@ function setupVideo(root=document){
}
}
// ----- Initial setup -----
setupGalleries(document);
setupAudio(document);
setupVideo(document);
// Observe dynamically added nodes
const dynamicObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (node.querySelectorAll){
setupGalleries(node);
setupAudio(node);
setupVideo(node);
}
if (node.querySelectorAll) {
setupGalleries(node);
setupAudio(node);
setupVideo(node);
}
}
}
});
dynamicObserver.observe(document.body, { childList: true, subtree: true });
// Watch URL changes and re-init
let lastUrl = location.href;
const urlObserver = new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
console.log('[gallery-init] URL changed → reinitializing galleries');
console.log('[gallery-init] URL changed → reinitializing');
setupGalleries(document);
setupAudio(document);
setupVideo(document);
}
});
urlObserver.observe(document, { subtree: true, childList: true });
// Expose helpers for tab-system.js
window._gallerySetupVideo = setupVideo;
window._gallerySetupAudio = setupAudio;
});

File diff suppressed because one or more lines are too long

2811
assets/scripts/mermaid.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1480
assets/scripts/script.js Executable file → Normal file

File diff suppressed because it is too large Load Diff

0
assets/scripts/search.js Executable file → Normal file
View File

168
assets/scripts/sidebar.js Normal file
View File

@@ -0,0 +1,168 @@
/* =========================================================
SIDEBAR — reads /assets/sidebar-tree.json
Falls back to search-index.json if tree unavailable.
sidebar-tree.json shape:
{
"groups": [
{ "label": "java", "files": [{ "title": "...", "url": "..." }, ...] },
...
{ "label": "uncategorised", "files": [...] }
]
}
========================================================= */
async function buildSidebar() {
const sidebar = document.getElementById("sidebar");
if (!sidebar) return;
const currentPath = location.pathname;
// --- Files label (kept from original) ---
const filesLabel = el("div", { class: "sidebar-section-label" });
filesLabel.textContent = "files";
sidebar.appendChild(filesLabel);
const tree = el("div", { id: "file-tree" });
sidebar.appendChild(tree);
// --- Try sidebar-tree.json first ---
let groups = null;
try {
const res = await fetch("/assets/sidebar-tree.json");
if (res.ok) {
const data = await res.json();
if (Array.isArray(data.groups) && data.groups.length) {
groups = data.groups;
}
}
} catch (_) { /* fall through */ }
// --- Fallback: build a single flat group from search-index.json ---
if (!groups) {
groups = await buildGroupsFromSearchIndex();
}
if (!groups || !groups.length) {
const empty = el("div", { class: "tree-folder" });
empty.style.cssText = "opacity:.4;font-size:.7rem;padding:8px 14px;";
empty.textContent = "no notes found";
tree.appendChild(empty);
return;
}
// --- Render each group as a collapsible folder ---
groups.forEach(group => {
const { label, files } = group;
if (!files || !files.length) return;
// Folder row
const folderEl = el("div", {
class: "tree-folder",
"data-folder": label,
});
const icon = el("span", { class: "tree-icon" });
icon.textContent = "▾";
const labelSpan = el("span", { class: "tree-folder-label" });
labelSpan.textContent = label;
const countBadge = el("span", { class: "tree-folder-count" });
countBadge.textContent = files.length;
folderEl.append(icon, labelSpan, countBadge);
// Children container
const children = el("div", { class: "tree-children" });
files.forEach(f => {
const pathname = normalisePathname(f.url);
children.appendChild(makeFileItem({ title: f.title, pathname }, currentPath));
});
// Collapse / expand
folderEl.addEventListener("click", () => {
const isOpen = children.style.display !== "none";
children.style.display = isOpen ? "none" : "";
icon.textContent = isOpen ? "▸" : "▾";
folderEl.classList.toggle("collapsed", isOpen);
});
tree.appendChild(folderEl);
tree.appendChild(children);
});
// --- Pinned section (unchanged from original) ---
try {
const res = await fetch("/assets/search-index.json");
if (res.ok) {
const raw = await res.json();
const all = Array.isArray(raw) ? raw : (raw.entries || raw.notes || []);
const pinned = all
.filter(e => /\/(inbox|daily[-_]?log|journal|home|index|moc)\.html$/i
.test(e.url || e.path || e.pathname || ""))
.map(e => ({
title: e.title || filenameToTitle(e.url || e.path || e.pathname),
pathname: normalisePathname(e.url || e.path || e.pathname),
}));
if (pinned.length) {
const divider = el("div", { class: "sidebar-divider" });
const pinnedLabel = el("div", { class: "sidebar-section-label" });
pinnedLabel.textContent = "pinned";
const pinnedWrap = el("div", { id: "sidebar-pinned" });
pinned.forEach(f => pinnedWrap.appendChild(makeFileItem(f, currentPath)));
sidebar.append(divider, pinnedLabel, pinnedWrap);
}
}
} catch (_) { /* pinned section is optional */ }
}
/* Fallback: turns search-index.json into a single "all notes" group */
async function buildGroupsFromSearchIndex() {
try {
const res = await fetch("/assets/search-index.json");
if (!res.ok) return null;
const raw = await res.json();
const all = Array.isArray(raw) ? raw : (raw.entries || raw.notes || []);
const files = all
.filter(e => e && (e.url || e.path || e.pathname))
.map(e => ({
title: e.title || filenameToTitle(e.url || e.path || e.pathname),
url: e.url || e.path || e.pathname,
}))
.sort((a, b) => a.title.localeCompare(b.title));
return files.length ? [{ label: "all notes", files }] : null;
} catch (_) {
return null;
}
}
/* =========================================================
CSS to paste into style.css (tree-folder-count badge
+ collapsed state)
=========================================================
Add this block anywhere after the existing .tree-folder rules:
.tree-folder-count {
margin-left: auto;
font-size: 0.65rem;
font-family: var(--font-mono);
color: var(--fg-faint);
background: var(--chip-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0 5px;
flex-shrink: 0;
line-height: 1.6;
}
.tree-folder.collapsed {
opacity: 0.75;
}
.tree-folder-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
========================================================= */

0
assets/scripts/svg-pan-zoom.min.js vendored Executable file → Normal file
View File

0
assets/styles/bigger-picture.min.css vendored Executable file → Normal file
View File

383
assets/styles/media.css Executable file → Normal file
View File

@@ -1,78 +1,317 @@
@media (max-width: 768px){
.banner-header{ flex-direction: column; align-items: center; text-align: center; }
.banner-left{ margin: 0 0 .5rem 0; }
.banner-logo{ margin: 0; }
nav{ flex-wrap: wrap; justify-content: center; gap: .5rem; font-size: 1rem; }
/* =========================================================
MEDIA.CSS — responsive overrides for the sidebar+tab layout
This file replaces the old float-based media queries.
Load it AFTER style.css.
========================================================= */
/* =========================================================
SIDENOTES / MARGINNOTES — base styles
========================================================= */
.theme-toggle {
position: static;
order: 2;
margin-left: .5rem;
padding: .3rem .6rem;
background: transparent;
}
.banner-header {
flex-wrap: wrap;
justify-content: center;
}
.banner-header nav {
display: flex;
align-items: center;
flex-wrap: wrap;
}
body.no-sidenotes {
margin-left: 1em;
margin-right: 1em;
}
#mobile-move-panel {
display: block;
}
.sidenote,
.marginnote {
font-size: 0.78rem;
line-height: 1.5;
color: var(--fg-dim);
font-family: var(--font-serif);
font-style: italic;
}
@media (max-width: 1250px){
#preamble.status{
padding-right: var(--body-pad);
}
#content.content{
padding-right: var(--body-pad);
}
.sidenote,
.marginnote{
float: none;
clear: both;
width: auto;
display: block;
margin: 0.5rem 0 0.75rem;
padding-left: 0.75rem;
border-left: 3px dotted color-mix(in oklab, var(--fg) 8%, transparent);
margin-right: 0;
}
.fullwidth{
max-width: calc(100vw - 2 * var(--body-pad));
}
.sidenote, .marginnote{
border-left: 3px dotted color-mix(in oklab, var(--fg) 12%, transparent);
}
.sidenote .mn-img,
.marginnote .mn-img{
border-color: color-mix(in oklab, var(--fg) 12%, transparent);
}
#table-of-contents{
float: none;
position: static;
width: auto;
margin: 0 0 1rem;
padding: .5rem .75rem;
border-right: 0;
border-left: 3px dotted color-mix(in oklab, var(--fg) 12%, transparent);
background: color-mix(in oklab, var(--bg) 96%, var(--fg) 4%);
border-radius: 4px;
/* =========================================================
CUSTOM SCROLLBARS — themed throughout
========================================================= */
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--border-mid) transparent;
}
/* Webkit (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-mid);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--fg-faint);
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* =========================================================
1200px — content column is narrow enough to collapse sidenotes
========================================================= */
@media (max-width: 1200px) {
.sidenote,
.marginnote {
float: none;
clear: both;
width: auto;
display: block;
margin: 0.5rem 0 0.75rem;
padding-left: 0.75rem;
border-left: 3px dotted color-mix(in oklab, var(--fg) 12%, transparent);
margin-right: 0;
border-radius: 0;
}
.sidenote .mn-img,
.marginnote .mn-img {
border-color: color-mix(in oklab, var(--fg) 12%, transparent);
}
.fullwidth {
max-width: calc(100vw - var(--sidebar-w) - 4rem);
}
#table-of-contents {
float: none;
position: static;
width: auto;
margin: 0 0 1rem;
padding: 0.5rem 0.75rem;
border-right: 0;
border-left: 3px dotted color-mix(in oklab, var(--fg) 12%, transparent);
background: color-mix(in oklab, var(--bg) 96%, var(--fg) 4%);
border-radius: 4px;
}
}
/* =========================================================
900px — auto-hide sidebar (collapses to 0 width)
========================================================= */
@media (max-width: 900px) {
#sidebar {
width: 0;
border-right: none;
overflow: hidden;
}
.fullwidth {
max-width: calc(100vw - 2rem);
}
}
/* =========================================================
768px — MOBILE: full single-column, overlay sidebar drawer
========================================================= */
@media (max-width: 768px) {
/* Allow body to scroll normally */
html, body {
overflow: auto;
height: auto;
}
body {
min-height: 100dvh;
height: auto;
}
/* ---- Header ---- */
#preamble {
height: auto;
position: sticky;
top: 0;
z-index: 300;
}
.banner-header {
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
height: auto;
min-height: var(--header-h);
}
.banner-left {
flex-direction: row;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.banner-logo {
height: 26px;
width: 26px;
}
#updated { display: none; }
nav { display: none; }
.web-logo { display: none; }
.banner-search {
order: 3;
flex: 1 1 100%;
max-width: 100%;
width: 100%;
}
.banner-actions {
margin-left: 0;
flex-shrink: 0;
}
/* ---- Layout ---- */
#layout-body {
flex-direction: column;
overflow: visible;
position: relative;
}
/* ---- Sidebar overlay drawer ---- */
#sidebar {
position: fixed;
top: 0;
left: 0;
width: 80vw;
max-width: 300px;
height: 100dvh;
z-index: 400;
border-right: 1px solid var(--border-mid);
overflow-y: auto;
overflow-x: hidden;
transform: translateX(-100%);
transition: transform var(--dur-mid) var(--ease-out),
box-shadow var(--dur-mid) var(--ease-out);
box-shadow: none;
}
#sidebar.mobile-open {
transform: translateX(0);
box-shadow: 4px 0 32px rgba(0, 0, 0, 0.5);
}
/* Dimmed backdrop — clicking it closes the sidebar */
#sidebar-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 390;
cursor: pointer;
}
#sidebar-backdrop.visible {
display: block;
}
/* ---- Main ---- */
#main {
width: 100%;
min-height: calc(100dvh - var(--header-h));
}
#tab-bar {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* ---- Content area ---- */
#content-area {
overflow: visible;
height: auto;
min-height: calc(100dvh - var(--header-h) - var(--tab-h));
background-attachment: scroll;
}
#content-area::before {
display: none;
}
/* ---- Note content ---- */
#content,
.tab-content-inner {
padding: 1.25rem 1rem 4rem;
}
.title-section {
padding: 1rem;
border-radius: 6px;
}
.title-section .title {
font-size: 1.2rem;
}
.sidenote,
.marginnote {
float: none;
clear: both;
width: auto;
display: block;
margin: 0.4rem 0 0.6rem;
padding-left: 0.6rem;
border-left: 3px dotted color-mix(in oklab, var(--fg) 12%, transparent);
font-size: 0.75rem;
}
.fullwidth {
max-width: calc(100vw - 2rem);
}
#table-of-contents {
float: none;
position: static;
width: auto;
margin: 0 0 1rem;
padding: 0.5rem 0.75rem;
border-right: 0;
border-left: 3px dotted color-mix(in oklab, var(--fg) 12%, transparent);
background: color-mix(in oklab, var(--bg) 96%, var(--fg) 4%);
border-radius: 4px;
font-size: 0.85rem;
}
/* ---- Footer ---- */
#postamble,
footer {
padding: 0.75rem 1rem;
font-size: 0.68rem;
}
/* ---- Code blocks ---- */
pre.src,
.org-src-container > pre {
font-size: 0.78rem;
}
/* ---- Tabs ---- */
.tab {
min-width: 70px;
padding: 0 8px 0 10px;
font-size: 0.68rem;
}
}
/* =========================================================
400px — very small phones
========================================================= */
@media (max-width: 400px) {
.banner-header {
padding: 0.4rem 0.75rem;
}
#content,
.tab-content-inner {
padding: 1rem 0.75rem 3rem;
}
.tab {
min-width: 60px;
font-size: 0.65rem;
}
}

0
assets/styles/org.css Executable file → Normal file
View File

1624
assets/styles/style.css Executable file → Normal file

File diff suppressed because it is too large Load Diff

0
assets/swappy-20250805-152411.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB