mirror of
https://github.com/perstarkse/minne.git
synced 2026-07-03 03:21:37 +02:00
refactor: split knowledge-graph monolith and extract rubberbanding logic
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Refactor: split knowledge-graph.js monolith into focused functions (loadGraphData, buildSvg, createSimulation, drawLinks/Nodes/Labels, createHighlighting, createZoom, attachResize); fixed dead duplicate zoom instance
|
||||||
|
- Refactor: extracted rubberbanding scroll logic in design-polish.js into standalone attachRubberbanding helper; removed dead pullDistance state
|
||||||
|
|
||||||
- Evaluations: simplified crate layout — linear pipeline, sharded-only converted store, in-memory ingestion, `db/` and `cli/` modules; namespace reuse state in corpus manifest (removed `cache/snapshots/`); no legacy JSON/history compatibility (re-run `--warm` after upgrade)
|
- Evaluations: simplified crate layout — linear pipeline, sharded-only converted store, in-memory ingestion, `db/` and `cli/` modules; namespace reuse state in corpus manifest (removed `cache/snapshots/`); no legacy JSON/history compatibility (re-run `--warm` after upgrade)
|
||||||
- Performance: ingestion skips per-task index rebuild; worker runs scheduled `REBUILD INDEX` (default every 24h via `index_rebuild_interval_secs`, `0` disables)
|
- Performance: ingestion skips per-task index rebuild; worker runs scheduled `REBUILD INDEX` (default every 24h via `index_rebuild_interval_secs`, `0` disables)
|
||||||
- Performance: ingestion persists all artifacts in a single SurrealDB transaction per task (atomic replace by task id)
|
- Performance: ingestion persists all artifacts in a single SurrealDB transaction per task (atomic replace by task id)
|
||||||
|
|||||||
+182
-159
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Design Polishing Pass - Interactive Effects
|
* Design Polishing Pass - Interactive Effects
|
||||||
*
|
*
|
||||||
* Includes:
|
* Includes:
|
||||||
* - Scroll-Linked Navbar Shadow
|
* - Scroll-Linked Navbar Shadow
|
||||||
* - HTMX Swap Animation
|
* - HTMX Swap Animation
|
||||||
@@ -8,183 +8,207 @@
|
|||||||
* - Rubberbanding Scroll
|
* - Rubberbanding Scroll
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function() {
|
(() => {
|
||||||
'use strict';
|
// === SCROLL-LINKED NAVBAR SHADOW ===
|
||||||
|
function initScrollShadow() {
|
||||||
|
const mainContent = document.querySelector("main");
|
||||||
|
const navbar = document.querySelector("nav");
|
||||||
|
if (!mainContent || !navbar) return;
|
||||||
|
|
||||||
// === SCROLL-LINKED NAVBAR SHADOW ===
|
mainContent.addEventListener(
|
||||||
function initScrollShadow() {
|
"scroll",
|
||||||
const mainContent = document.querySelector('main');
|
() => {
|
||||||
const navbar = document.querySelector('nav');
|
const scrollTop = mainContent.scrollTop;
|
||||||
if (!mainContent || !navbar) return;
|
const scrollHeight =
|
||||||
|
mainContent.scrollHeight - mainContent.clientHeight;
|
||||||
|
const scrollDepth = scrollHeight > 0 ? Math.min(scrollTop / 200, 1) : 0;
|
||||||
|
navbar.style.setProperty("--scroll-depth", scrollDepth.toFixed(2));
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
mainContent.addEventListener('scroll', () => {
|
// === HTMX SWAP ANIMATION ===
|
||||||
const scrollTop = mainContent.scrollTop;
|
function initHtmxSwapAnimation() {
|
||||||
const scrollHeight = mainContent.scrollHeight - mainContent.clientHeight;
|
document.body.addEventListener("htmx:afterSwap", (event) => {
|
||||||
const scrollDepth = scrollHeight > 0 ? Math.min(scrollTop / 200, 1) : 0;
|
let target = event.detail.target;
|
||||||
navbar.style.setProperty('--scroll-depth', scrollDepth.toFixed(2));
|
if (!target) return;
|
||||||
}, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HTMX SWAP ANIMATION ===
|
// If full body swap (hx-boost), animate only the main content
|
||||||
function initHtmxSwapAnimation() {
|
if (target.tagName === "BODY") {
|
||||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
const main = document.querySelector("main");
|
||||||
let target = event.detail.target;
|
if (main) target = main;
|
||||||
if (!target) return;
|
}
|
||||||
|
|
||||||
// If full body swap (hx-boost), animate only the main content
|
// Only animate if target is valid and inside/is main content or a card/panel
|
||||||
if (target.tagName === 'BODY') {
|
// Avoid animating sidebar or navbar updates
|
||||||
const main = document.querySelector('main');
|
if (target && (target.tagName === "MAIN" || target.closest("main"))) {
|
||||||
if (main) target = main;
|
if (!target.classList.contains("animate-fade-up")) {
|
||||||
}
|
target.classList.add("animate-fade-up");
|
||||||
|
// Remove class after animation completes to allow re-animation
|
||||||
|
setTimeout(() => {
|
||||||
|
target.classList.remove("animate-fade-up");
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Only animate if target is valid and inside/is main content or a card/panel
|
// === TYPEWRITER AI RESPONSE ===
|
||||||
// Avoid animating sidebar or navbar updates
|
// Works with SSE streaming - buffers text and reveals character by character
|
||||||
if (target && (target.tagName === 'MAIN' || target.closest('main'))) {
|
window.initTypewriter = (element, options = {}) => {
|
||||||
if (!target.classList.contains('animate-fade-up')) {
|
const { minDelay = 5, maxDelay = 15, showCursor = true } = options;
|
||||||
target.classList.add('animate-fade-up');
|
|
||||||
// Remove class after animation completes to allow re-animation
|
|
||||||
setTimeout(() => {
|
|
||||||
target.classList.remove('animate-fade-up');
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === TYPEWRITER AI RESPONSE ===
|
let buffer = "";
|
||||||
// Works with SSE streaming - buffers text and reveals character by character
|
let isTyping = false;
|
||||||
window.initTypewriter = function(element, options = {}) {
|
let cursorElement = null;
|
||||||
const {
|
|
||||||
minDelay = 5,
|
|
||||||
maxDelay = 15,
|
|
||||||
showCursor = true
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
let buffer = '';
|
if (showCursor) {
|
||||||
let isTyping = false;
|
cursorElement = document.createElement("span");
|
||||||
let cursorElement = null;
|
cursorElement.className = "typewriter-cursor";
|
||||||
|
cursorElement.textContent = "▌";
|
||||||
|
cursorElement.style.animation = "blink 1s step-end infinite";
|
||||||
|
element.appendChild(cursorElement);
|
||||||
|
}
|
||||||
|
|
||||||
if (showCursor) {
|
function typeNextChar() {
|
||||||
cursorElement = document.createElement('span');
|
if (buffer.length === 0) {
|
||||||
cursorElement.className = 'typewriter-cursor';
|
isTyping = false;
|
||||||
cursorElement.textContent = '▌';
|
return;
|
||||||
cursorElement.style.animation = 'blink 1s step-end infinite';
|
}
|
||||||
element.appendChild(cursorElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeNextChar() {
|
isTyping = true;
|
||||||
if (buffer.length === 0) {
|
const char = buffer.charAt(0);
|
||||||
isTyping = false;
|
buffer = buffer.slice(1);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isTyping = true;
|
// Insert before cursor
|
||||||
const char = buffer.charAt(0);
|
if (cursorElement && cursorElement.parentNode) {
|
||||||
buffer = buffer.slice(1);
|
const textNode = document.createTextNode(char);
|
||||||
|
element.insertBefore(textNode, cursorElement);
|
||||||
|
} else {
|
||||||
|
element.textContent += char;
|
||||||
|
}
|
||||||
|
|
||||||
// Insert before cursor
|
const delay = minDelay + Math.random() * (maxDelay - minDelay);
|
||||||
if (cursorElement && cursorElement.parentNode) {
|
setTimeout(typeNextChar, delay);
|
||||||
const textNode = document.createTextNode(char);
|
}
|
||||||
element.insertBefore(textNode, cursorElement);
|
|
||||||
} else {
|
|
||||||
element.textContent += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = minDelay + Math.random() * (maxDelay - minDelay);
|
return {
|
||||||
setTimeout(typeNextChar, delay);
|
append: (text) => {
|
||||||
}
|
buffer += text;
|
||||||
|
if (!isTyping) {
|
||||||
|
typeNextChar();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
// Flush remaining buffer immediately
|
||||||
|
if (cursorElement && cursorElement.parentNode) {
|
||||||
|
const textNode = document.createTextNode(buffer);
|
||||||
|
element.insertBefore(textNode, cursorElement);
|
||||||
|
cursorElement.remove();
|
||||||
|
} else {
|
||||||
|
element.textContent += buffer;
|
||||||
|
}
|
||||||
|
buffer = "";
|
||||||
|
isTyping = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
// === RUBBERBANDING SCROLL ===
|
||||||
append: function(text) {
|
function attachRubberbanding(
|
||||||
buffer += text;
|
container,
|
||||||
if (!isTyping) {
|
{ maxPull = 60, resistance = 0.4 } = {},
|
||||||
typeNextChar();
|
) {
|
||||||
}
|
let startY = 0;
|
||||||
},
|
let pulling = false;
|
||||||
complete: function() {
|
|
||||||
// Flush remaining buffer immediately
|
|
||||||
if (cursorElement && cursorElement.parentNode) {
|
|
||||||
const textNode = document.createTextNode(buffer);
|
|
||||||
element.insertBefore(textNode, cursorElement);
|
|
||||||
cursorElement.remove();
|
|
||||||
} else {
|
|
||||||
element.textContent += buffer;
|
|
||||||
}
|
|
||||||
buffer = '';
|
|
||||||
isTyping = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// === RUBBERBANDING SCROLL ===
|
function applyPull(distance) {
|
||||||
function initRubberbanding() {
|
container.style.transform = `translateY(${distance}px)`;
|
||||||
const containers = document.querySelectorAll('#chat-scroll-container, .content-scroll-container');
|
}
|
||||||
|
|
||||||
containers.forEach(container => {
|
|
||||||
let startY = 0;
|
|
||||||
let pulling = false;
|
|
||||||
let pullDistance = 0;
|
|
||||||
const maxPull = 60;
|
|
||||||
const resistance = 0.4;
|
|
||||||
|
|
||||||
container.addEventListener('touchstart', (e) => {
|
function release() {
|
||||||
startY = e.touches[0].clientY;
|
container.style.transition =
|
||||||
}, { passive: true });
|
"transform 300ms cubic-bezier(0.25, 1, 0.5, 1)";
|
||||||
|
container.style.transform = "translateY(0)";
|
||||||
|
setTimeout(() => {
|
||||||
|
container.style.transition = "";
|
||||||
|
}, 300);
|
||||||
|
pulling = false;
|
||||||
|
}
|
||||||
|
|
||||||
container.addEventListener('touchmove', (e) => {
|
function isAtTop() {
|
||||||
const currentY = e.touches[0].clientY;
|
return container.scrollTop <= 0;
|
||||||
const diff = currentY - startY;
|
}
|
||||||
|
function isAtBottom() {
|
||||||
// At top boundary, pulling down
|
return (
|
||||||
if (container.scrollTop <= 0 && diff > 0) {
|
container.scrollTop + container.clientHeight >= container.scrollHeight
|
||||||
pulling = true;
|
);
|
||||||
pullDistance = Math.min(diff * resistance, maxPull);
|
}
|
||||||
container.style.transform = `translateY(${pullDistance}px)`;
|
|
||||||
}
|
|
||||||
// At bottom boundary, pulling up
|
|
||||||
else if (container.scrollTop + container.clientHeight >= container.scrollHeight && diff < 0) {
|
|
||||||
pulling = true;
|
|
||||||
pullDistance = Math.max(diff * resistance, -maxPull);
|
|
||||||
container.style.transform = `translateY(${pullDistance}px)`;
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
container.addEventListener('touchend', () => {
|
container.addEventListener(
|
||||||
if (pulling) {
|
"touchstart",
|
||||||
container.style.transition = 'transform 300ms cubic-bezier(0.25, 1, 0.5, 1)';
|
(e) => {
|
||||||
container.style.transform = 'translateY(0)';
|
startY = e.touches[0].clientY;
|
||||||
setTimeout(() => {
|
},
|
||||||
container.style.transition = '';
|
{ passive: true },
|
||||||
}, 300);
|
);
|
||||||
pulling = false;
|
|
||||||
pullDistance = 0;
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === INITIALIZATION ===
|
container.addEventListener(
|
||||||
function init() {
|
"touchmove",
|
||||||
initScrollShadow();
|
(e) => {
|
||||||
initHtmxSwapAnimation();
|
const diff = e.touches[0].clientY - startY;
|
||||||
initRubberbanding();
|
const isPullingDown = diff > 0 && isAtTop();
|
||||||
}
|
const isPullingUp = diff < 0 && isAtBottom();
|
||||||
|
|
||||||
// Run on DOMContentLoaded
|
if (isPullingDown) {
|
||||||
if (document.readyState === 'loading') {
|
pulling = true;
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
applyPull(Math.min(diff * resistance, maxPull));
|
||||||
} else {
|
} else if (isPullingUp) {
|
||||||
init();
|
pulling = true;
|
||||||
}
|
applyPull(Math.max(diff * resistance, -maxPull));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
|
||||||
// Re-init rubberbanding after HTMX navigations
|
container.addEventListener(
|
||||||
document.body.addEventListener('htmx:afterSettle', () => {
|
"touchend",
|
||||||
initRubberbanding();
|
() => {
|
||||||
});
|
if (pulling) release();
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add typewriter cursor blink animation
|
function initRubberbanding() {
|
||||||
const style = document.createElement('style');
|
document
|
||||||
style.textContent = `
|
.querySelectorAll("#chat-scroll-container, .content-scroll-container")
|
||||||
|
.forEach((container) => attachRubberbanding(container));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === INITIALIZATION ===
|
||||||
|
function init() {
|
||||||
|
initScrollShadow();
|
||||||
|
initHtmxSwapAnimation();
|
||||||
|
initRubberbanding();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run on DOMContentLoaded
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-init rubberbanding after HTMX navigations
|
||||||
|
document.body.addEventListener("htmx:afterSettle", () => {
|
||||||
|
initRubberbanding();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add typewriter cursor blink animation
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = `
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0; }
|
50% { opacity: 0; }
|
||||||
@@ -194,6 +218,5 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user