removed excess comments
This commit is contained in:
@@ -10,22 +10,18 @@ export const ActivityHeatmap: Component<{
|
||||
contributions: ContributionDay[] | undefined;
|
||||
title: string;
|
||||
}> = (props) => {
|
||||
// Generate last 12 weeks of days
|
||||
const weeks = createMemo(() => {
|
||||
const today = new Date();
|
||||
const weeksData: { date: string; count: number }[][] = [];
|
||||
|
||||
// Start from 12 weeks ago
|
||||
const startDate = new Date(today);
|
||||
startDate.setDate(startDate.getDate() - 84); // 12 weeks
|
||||
startDate.setDate(startDate.getDate() - 84);
|
||||
|
||||
// Create a map for quick lookup
|
||||
const contributionMap = new Map<string, number>();
|
||||
props.contributions?.forEach((c) => {
|
||||
contributionMap.set(c.date, c.count);
|
||||
});
|
||||
|
||||
// Generate weeks
|
||||
for (let week = 0; week < 12; week++) {
|
||||
const weekData: { date: string; count: number }[] = [];
|
||||
|
||||
@@ -68,7 +64,6 @@ export const ActivityHeatmap: Component<{
|
||||
when={props.contributions && props.contributions.length > 0}
|
||||
fallback={
|
||||
<div class="relative">
|
||||
{/* Skeleton grid matching heatmap dimensions */}
|
||||
<div class="flex gap-[2px]">
|
||||
<For each={Array(12)}>
|
||||
{() => (
|
||||
@@ -80,7 +75,6 @@ export const ActivityHeatmap: Component<{
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{/* Centered spinner overlay */}
|
||||
<div class="absolute inset-0 top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2">
|
||||
<SkeletonBox class="-ml-2 h-8 w-8" />
|
||||
</div>
|
||||
|
||||
@@ -26,20 +26,14 @@ function formatDomainName(url: string): string {
|
||||
return withoutWww.charAt(0).toUpperCase() + withoutWww.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a banner photo URL to its thumbnail version
|
||||
* Replaces the filename with -small variant (e.g., image.jpg -> image-small.jpg)
|
||||
*/
|
||||
function getThumbnailUrl(bannerPhoto: string | null): string {
|
||||
if (!bannerPhoto) return "/blueprint.jpg";
|
||||
|
||||
// Check if URL contains a file extension
|
||||
const match = bannerPhoto.match(/^(.+)(\.[^.]+)$/);
|
||||
if (match) {
|
||||
return `${match[1]}-small${match[2]}`;
|
||||
}
|
||||
|
||||
// Fallback to original if no extension found
|
||||
return bannerPhoto;
|
||||
}
|
||||
|
||||
@@ -77,7 +71,6 @@ export function RightBarContent() {
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
// Fetch all data client-side only to avoid hydration mismatch
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [ghCommits, gtCommits, ghActivity, gtActivity] =
|
||||
@@ -101,7 +94,6 @@ export function RightBarContent() {
|
||||
}
|
||||
};
|
||||
|
||||
// Defer API calls to next tick to allow initial render to complete first
|
||||
setTimeout(() => {
|
||||
fetchData();
|
||||
}, 0);
|
||||
@@ -168,7 +160,6 @@ export function RightBarContent() {
|
||||
</ul>
|
||||
</Typewriter>
|
||||
|
||||
{/* Git Activity Section */}
|
||||
<hr class="border-overlay0" />
|
||||
<div class="flex min-w-0 flex-col gap-6 px-4 pt-6">
|
||||
<RecentCommits
|
||||
@@ -266,28 +257,22 @@ export function LeftBar() {
|
||||
onMount(() => {
|
||||
setIsMounted(true);
|
||||
|
||||
// Set up window resize listener for reactive styling
|
||||
const handleResize = () => {
|
||||
setWindowWidth(window.innerWidth);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Terminal-style appearance animation for "Get Lost" button
|
||||
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
|
||||
const originalText = "What's this?";
|
||||
let glitchInterval: NodeJS.Timeout;
|
||||
|
||||
// Delay appearance to match terminal vibe
|
||||
setTimeout(() => {
|
||||
// Make visible immediately so typing animation is visible
|
||||
setGetLostVisible(true);
|
||||
|
||||
// Type-in animation with random characters resolving
|
||||
let currentIndex = 0;
|
||||
const typeInterval = setInterval(() => {
|
||||
if (currentIndex <= originalText.length) {
|
||||
let displayText = originalText.substring(0, currentIndex);
|
||||
// Add random trailing characters
|
||||
if (currentIndex < originalText.length) {
|
||||
const remaining = originalText.length - currentIndex;
|
||||
for (let i = 0; i < remaining; i++) {
|
||||
@@ -301,14 +286,11 @@ export function LeftBar() {
|
||||
clearInterval(typeInterval);
|
||||
setGetLostText(originalText);
|
||||
|
||||
// Start regular glitch effect after typing completes
|
||||
glitchInterval = setInterval(() => {
|
||||
if (Math.random() > 0.9) {
|
||||
// 10% chance to glitch
|
||||
let glitched = "";
|
||||
for (let i = 0; i < originalText.length; i++) {
|
||||
if (Math.random() > 0.7) {
|
||||
// 30% chance each character glitches
|
||||
glitched +=
|
||||
glitchChars[Math.floor(Math.random() * glitchChars.length)];
|
||||
} else {
|
||||
@@ -323,11 +305,10 @@ export function LeftBar() {
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}, 140); // Type speed (higher is slower)
|
||||
}, 500); // Initial delay before appearing
|
||||
}, 140);
|
||||
}, 500);
|
||||
|
||||
if (ref) {
|
||||
// Focus trap for accessibility on mobile
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isMobile = window.innerWidth < BREAKPOINTS.MOBILE;
|
||||
|
||||
@@ -346,13 +327,11 @@ export function LeftBar() {
|
||||
] as HTMLElement;
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift+Tab - going backwards
|
||||
if (document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab - going forwards
|
||||
if (document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
@@ -392,12 +371,9 @@ export function LeftBar() {
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Refetch user info whenever location changes
|
||||
createEffect(() => {
|
||||
// Track location changes
|
||||
location.pathname;
|
||||
|
||||
// Only refetch if component is mounted
|
||||
if (isMounted()) {
|
||||
fetchUserInfo();
|
||||
}
|
||||
@@ -433,7 +409,6 @@ export function LeftBar() {
|
||||
}}
|
||||
style={getMainNavStyles()}
|
||||
>
|
||||
{/* Hamburger menu button - positioned at right edge of navbar */}
|
||||
<button
|
||||
onClick={() => setLeftBarVisible(!leftBarVisible())}
|
||||
class="hamburger-menu-btn absolute top-4 -right-14 z-9999 rounded-md p-2 shadow-md backdrop-blur-2xl transition-transform duration-600 ease-in-out hover:scale-110"
|
||||
@@ -442,7 +417,7 @@ export function LeftBar() {
|
||||
}}
|
||||
aria-label="Toggle navigation menu"
|
||||
style={{
|
||||
display: "none" // Hidden by default, shown via media query for non-touch devices
|
||||
display: "none"
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -507,7 +482,6 @@ export function LeftBar() {
|
||||
alt="post-cover"
|
||||
class="float-right mb-1 ml-2 h-12 w-16 rounded object-cover"
|
||||
onError={(e) => {
|
||||
// Fallback to full banner if thumbnail doesn't exist
|
||||
const img = e.currentTarget;
|
||||
if (
|
||||
img.src !==
|
||||
@@ -537,7 +511,6 @@ export function LeftBar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div class="mt-auto">
|
||||
<Typewriter keepAlive={false}>
|
||||
<ul class="flex flex-col gap-4 pt-6">
|
||||
@@ -591,7 +564,6 @@ export function LeftBar() {
|
||||
</ul>
|
||||
</Typewriter>
|
||||
|
||||
{/* Get Lost button - outside Typewriter to allow glitch effect */}
|
||||
<ul class="pt-4 pb-6">
|
||||
<li
|
||||
class="hover:text-subtext0 w-fit transition-all duration-500 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
|
||||
|
||||
@@ -38,7 +38,6 @@ export function Btop(props: BtopProps) {
|
||||
const [isMobile, setIsMobile] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
// Check if mobile
|
||||
if (typeof window !== "undefined") {
|
||||
setIsMobile(window.innerWidth < BREAKPOINTS.MOBILE);
|
||||
|
||||
@@ -49,7 +48,6 @@ export function Btop(props: BtopProps) {
|
||||
onCleanup(() => window.removeEventListener("resize", handleResize));
|
||||
}
|
||||
|
||||
// Animate CPU usage
|
||||
const cpuInterval = setInterval(() => {
|
||||
setCpuUsage((prev) => {
|
||||
const change = (Math.random() - 0.5) * 10;
|
||||
@@ -58,7 +56,6 @@ export function Btop(props: BtopProps) {
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// Animate memory usage
|
||||
const memInterval = setInterval(() => {
|
||||
setMemUsage((prev) => {
|
||||
const change = (Math.random() - 0.5) * 5;
|
||||
@@ -67,13 +64,11 @@ export function Btop(props: BtopProps) {
|
||||
});
|
||||
}, 1500);
|
||||
|
||||
// Animate network
|
||||
const netInterval = setInterval(() => {
|
||||
setNetDown(Math.floor(Math.random() * 1000));
|
||||
setNetUp(Math.floor(Math.random() * 100));
|
||||
}, 800);
|
||||
|
||||
// Animate processes
|
||||
const procInterval = setInterval(() => {
|
||||
setProcesses((prev) =>
|
||||
prev.map((proc) => ({
|
||||
@@ -90,12 +85,10 @@ export function Btop(props: BtopProps) {
|
||||
);
|
||||
}, 2000);
|
||||
|
||||
// Keyboard handler for :q
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (!isMobile() && e.key === "q" && e.shiftKey && e.key === ":") {
|
||||
props.onClose();
|
||||
}
|
||||
// Simple 'q' press to quit
|
||||
if (!isMobile() && e.key === "q") {
|
||||
props.onClose();
|
||||
}
|
||||
@@ -120,9 +113,7 @@ export function Btop(props: BtopProps) {
|
||||
|
||||
return (
|
||||
<div class="bg-crust fixed inset-0 z-[10000] flex items-center justify-center p-4">
|
||||
{/* Main btop container */}
|
||||
<div class="bg-mantle border-surface0 text-text relative h-full w-full max-w-6xl overflow-hidden rounded-lg border-2 font-mono text-sm shadow-2xl md:h-auto md:max-h-[90vh]">
|
||||
{/* Header */}
|
||||
<div class="border-surface0 bg-surface0 border-b px-4 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-blue">
|
||||
@@ -145,13 +136,10 @@ export function Btop(props: BtopProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div class="space-y-4 p-4">
|
||||
{/* System Stats */}
|
||||
<div class="border-surface0 bg-base rounded border p-3">
|
||||
<div class="text-green mb-2 font-bold">System Resources</div>
|
||||
<div class="space-y-2">
|
||||
{/* CPU */}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-subtext1 w-12">CPU</span>
|
||||
<span class="text-blue">[{createBar(cpuUsage())}]</span>
|
||||
@@ -159,7 +147,6 @@ export function Btop(props: BtopProps) {
|
||||
<span class="text-subtext0">2.4 GHz</span>
|
||||
</div>
|
||||
|
||||
{/* Memory */}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-subtext1 w-12">MEM</span>
|
||||
<span class="text-blue">[{createBar(memUsage())}]</span>
|
||||
@@ -169,7 +156,6 @@ export function Btop(props: BtopProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Network */}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-subtext1 w-12">NET</span>
|
||||
<span class="text-green">↓ {netDown()} KB/s</span>
|
||||
@@ -178,7 +164,6 @@ export function Btop(props: BtopProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Process List */}
|
||||
<div class="border-surface0 bg-base rounded border">
|
||||
<div class="border-surface0 border-b px-3 py-2">
|
||||
<span class="text-green font-bold">Processes</span>
|
||||
@@ -224,7 +209,6 @@ export function Btop(props: BtopProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div class="text-subtext1 text-center text-xs">
|
||||
<Show
|
||||
when={!isMobile()}
|
||||
@@ -238,7 +222,6 @@ export function Btop(props: BtopProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay background */}
|
||||
<div
|
||||
class="absolute inset-0 -z-10 bg-black/80 backdrop-blur-sm"
|
||||
onClick={props.onClose}
|
||||
|
||||
@@ -19,7 +19,6 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
props.initialRemainingTime ?? props.duration
|
||||
);
|
||||
|
||||
// Calculate progress (0 to 1)
|
||||
const progress = () => remainingTime() / props.duration;
|
||||
const strokeDashoffset = () => circumference * (1 - progress());
|
||||
|
||||
@@ -62,7 +61,6 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
height={props.size}
|
||||
style={{ transform: "rotate(-90deg)" }}
|
||||
>
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx={props.size / 2}
|
||||
cy={props.size / 2}
|
||||
@@ -71,7 +69,6 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
stroke="#e5e7eb"
|
||||
stroke-width={props.strokeWidth}
|
||||
/>
|
||||
{/* Progress circle */}
|
||||
<circle
|
||||
cx={props.size / 2}
|
||||
cy={props.size / 2}
|
||||
@@ -84,7 +81,6 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
{/* Timer text in center */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
|
||||
@@ -28,17 +28,14 @@ export default function CustomScrollbar(props: CustomScrollbarProps) {
|
||||
const scrollHeight = containerRef.scrollHeight;
|
||||
const clientHeight = containerRef.clientHeight;
|
||||
|
||||
// Calculate thumb height as percentage of visible area
|
||||
const viewportRatio = clientHeight / scrollHeight;
|
||||
const calculatedThumbHeight = Math.max(viewportRatio * 100, 5);
|
||||
setThumbHeight(calculatedThumbHeight);
|
||||
|
||||
// Calculate scroll percentage
|
||||
const maxScroll = scrollHeight - clientHeight;
|
||||
const percentage = maxScroll > 0 ? (scrollTop / maxScroll) * 100 : 0;
|
||||
setScrollPercentage(percentage);
|
||||
|
||||
// Show scrollbar on scroll if autoHide enabled
|
||||
if (props.autoHide) {
|
||||
setIsVisible(true);
|
||||
clearTimeout(hideTimeout);
|
||||
@@ -106,10 +103,8 @@ export default function CustomScrollbar(props: CustomScrollbarProps) {
|
||||
onMount(() => {
|
||||
if (!containerRef) return;
|
||||
|
||||
// Initial update
|
||||
updateScrollbar();
|
||||
|
||||
// Update after delays to catch dynamically loaded content
|
||||
setTimeout(() => updateScrollbar(), 100);
|
||||
setTimeout(() => updateScrollbar(), 500);
|
||||
|
||||
@@ -118,7 +113,6 @@ export default function CustomScrollbar(props: CustomScrollbarProps) {
|
||||
updateScrollbar();
|
||||
};
|
||||
|
||||
// Debounced mutation observer
|
||||
let mutationTimeout: NodeJS.Timeout;
|
||||
const observer = new MutationObserver(() => {
|
||||
clearTimeout(mutationTimeout);
|
||||
@@ -132,7 +126,6 @@ export default function CustomScrollbar(props: CustomScrollbarProps) {
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Use passive scroll listener for better performance
|
||||
containerRef.addEventListener("scroll", updateScrollbar, { passive: true });
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
@@ -159,7 +152,6 @@ export default function CustomScrollbar(props: CustomScrollbarProps) {
|
||||
"-ms-overflow-style": "none"
|
||||
}}
|
||||
>
|
||||
{/* Hide default scrollbar */}
|
||||
<style>
|
||||
{`
|
||||
div::-webkit-scrollbar {
|
||||
@@ -170,7 +162,6 @@ export default function CustomScrollbar(props: CustomScrollbarProps) {
|
||||
|
||||
{props.children}
|
||||
|
||||
{/* Custom scrollbar */}
|
||||
<Show when={thumbHeight() < 100}>
|
||||
<div
|
||||
ref={scrollbarRef}
|
||||
|
||||
@@ -9,11 +9,9 @@ export default function DeletionForm() {
|
||||
const [error, setError] = createSignal("");
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
|
||||
// Form ref
|
||||
let emailRef: HTMLInputElement | undefined;
|
||||
let timerInterval: number | undefined;
|
||||
|
||||
// Calculate remaining time from cookie
|
||||
const calcRemainder = (timer: string) => {
|
||||
const expires = new Date(timer);
|
||||
const remaining = expires.getTime() - Date.now();
|
||||
@@ -92,7 +90,6 @@ export default function DeletionForm() {
|
||||
}
|
||||
};
|
||||
|
||||
// Countdown timer render function
|
||||
const renderTime = ({ remainingTime }: { remainingTime: number }) => {
|
||||
return (
|
||||
<div class="timer">
|
||||
|
||||
@@ -20,13 +20,11 @@ export default function ErrorBoundaryFallback(
|
||||
};
|
||||
}
|
||||
|
||||
// Try to get dark mode, fallback to a function returning true (dark) if context unavailable
|
||||
let isDark: () => boolean;
|
||||
try {
|
||||
const darkMode = useDarkMode();
|
||||
isDark = darkMode.isDark;
|
||||
} catch (e) {
|
||||
// Context not available, use default
|
||||
isDark = () => true;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ export default function PasswordStrengthMeter(
|
||||
});
|
||||
}
|
||||
|
||||
// Always show special character as optional/recommended
|
||||
reqs.push({
|
||||
label: "One special character\n(recommended)",
|
||||
test: (pwd) => /[^A-Za-z0-9]/.test(pwd),
|
||||
|
||||
@@ -52,7 +52,6 @@ function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
"will-change": "transform"
|
||||
}));
|
||||
|
||||
// Set up animation when component mounts or when direction/speed changes
|
||||
createEffect(() => {
|
||||
if (!containerRef) return;
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export function TerminalErrorPage(props: TerminalErrorPageProps) {
|
||||
let inputRef: HTMLInputElement | undefined;
|
||||
let footerRef: HTMLDivElement | undefined;
|
||||
|
||||
// Auto-scroll to bottom when history changes
|
||||
createEffect(() => {
|
||||
if (history().length > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -21,7 +21,6 @@ export function Typewriter(props: {
|
||||
|
||||
containerRef.style.position = "relative";
|
||||
|
||||
// FIRST: Walk DOM and split text into character spans
|
||||
const textNodes: { node: Text; text: string; startIndex: number }[] = [];
|
||||
let totalChars = 0;
|
||||
|
||||
@@ -36,12 +35,10 @@ export function Typewriter(props: {
|
||||
});
|
||||
totalChars += text.length;
|
||||
|
||||
// Replace text with spans for each character
|
||||
const span = document.createElement("span");
|
||||
text.split("").forEach((char, i) => {
|
||||
const charSpan = document.createElement("span");
|
||||
charSpan.textContent = char;
|
||||
// Don't set opacity here - CSS will handle it based on data-typewriter state
|
||||
charSpan.setAttribute(
|
||||
"data-char-index",
|
||||
String(totalChars - text.length + i)
|
||||
@@ -57,21 +54,18 @@ export function Typewriter(props: {
|
||||
|
||||
walkDOM(containerRef);
|
||||
|
||||
// Mark as animated AFTER DOM manipulation - this triggers CSS to hide characters
|
||||
setAnimated(true);
|
||||
|
||||
containerRef.setAttribute("data-typewriter-ready", "true");
|
||||
|
||||
// Listen for animation end to hide cursor
|
||||
const handleAnimationEnd = () => {
|
||||
setShouldHide(true);
|
||||
cursorRef?.removeEventListener("animationend", handleAnimationEnd);
|
||||
};
|
||||
|
||||
const startReveal = () => {
|
||||
setIsTyping(true); // Switch to typing cursor
|
||||
setIsTyping(true);
|
||||
|
||||
// Animate revealing characters
|
||||
let currentIndex = 0;
|
||||
const speed = props.speed || 30;
|
||||
|
||||
@@ -88,7 +82,6 @@ export function Typewriter(props: {
|
||||
const rect = charSpan.getBoundingClientRect();
|
||||
const containerRect = containerRef.getBoundingClientRect();
|
||||
|
||||
// Position cursor at the end of the current character
|
||||
cursorRef.style.left = `${rect.right - containerRect.left}px`;
|
||||
cursorRef.style.top = `${rect.top - containerRect.top}px`;
|
||||
cursorRef.style.height = `${charSpan.offsetHeight}px`;
|
||||
@@ -98,15 +91,11 @@ export function Typewriter(props: {
|
||||
currentIndex++;
|
||||
setTimeout(revealNextChar, 1000 / speed);
|
||||
} else {
|
||||
// Typing finished, switch to block cursor
|
||||
setIsTyping(false);
|
||||
|
||||
// Start keepAlive timer if it's a number
|
||||
if (typeof keepAlive === "number") {
|
||||
// Attach animation end listener
|
||||
cursorRef?.addEventListener("animationend", handleAnimationEnd);
|
||||
|
||||
// Trigger the animation with finite iteration count
|
||||
const durationSeconds = keepAlive / 1000;
|
||||
const iterations = Math.ceil(durationSeconds);
|
||||
if (cursorRef) {
|
||||
|
||||
@@ -25,16 +25,13 @@ export default function CommentBlock(props: CommentBlockProps) {
|
||||
const [deletionLoading, setDeletionLoading] = createSignal(false);
|
||||
const [userData, setUserData] = createSignal<UserPublicData | null>(null);
|
||||
|
||||
// Refs
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
let commentInputRef: HTMLDivElement | undefined;
|
||||
|
||||
// Auto-collapse at level 4+
|
||||
createEffect(() => {
|
||||
setCommentCollapsed(props.level >= 4);
|
||||
});
|
||||
|
||||
// Find user data from comment map
|
||||
createEffect(() => {
|
||||
if (props.userCommentMap) {
|
||||
props.userCommentMap.forEach((commentIds, user) => {
|
||||
@@ -45,23 +42,19 @@ export default function CommentBlock(props: CommentBlockProps) {
|
||||
}
|
||||
});
|
||||
|
||||
// Update toggle height based on container size
|
||||
createEffect(() => {
|
||||
if (containerRef) {
|
||||
const correction = showingReactionOptions() ? 80 : 48;
|
||||
setToggleHeight(containerRef.clientHeight + correction);
|
||||
}
|
||||
// Trigger on these dependencies
|
||||
windowWidth();
|
||||
showingReactionOptions();
|
||||
});
|
||||
|
||||
// Update reactions from map
|
||||
createEffect(() => {
|
||||
setReactions(props.reactionMap.get(props.comment.id) || []);
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
const collapseCommentToggle = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -112,7 +105,6 @@ export default function CommentBlock(props: CommentBlockProps) {
|
||||
);
|
||||
};
|
||||
|
||||
// Computed values
|
||||
const upvoteCount = () =>
|
||||
reactions().filter((r) => r.type === "upVote").length;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { CommentInputBlockProps } from "~/types/comment";
|
||||
export default function CommentInputBlock(props: CommentInputBlockProps) {
|
||||
let bodyRef: HTMLTextAreaElement | undefined;
|
||||
|
||||
// Clear the textarea when comment is submitted
|
||||
createEffect(() => {
|
||||
if (!props.commentSubmitLoading && bodyRef) {
|
||||
bodyRef.value = "";
|
||||
|
||||
@@ -8,14 +8,12 @@ export default function CommentSorting(props: CommentSortingProps) {
|
||||
new Map(props.topLevelComments?.map((comment) => [comment.id, true]))
|
||||
);
|
||||
|
||||
// Update showing block when top level comments change
|
||||
createEffect(() => {
|
||||
setShowingBlock(
|
||||
new Map(props.topLevelComments?.map((comment) => [comment.id, true]))
|
||||
);
|
||||
});
|
||||
|
||||
// Reset clickedOnce after timeout
|
||||
createEffect(() => {
|
||||
if (clickedOnce()) {
|
||||
setTimeout(() => setClickedOnce(false), 300);
|
||||
@@ -34,7 +32,6 @@ export default function CommentSorting(props: CommentSortingProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Comments are already sorted from server, no need for client-side sorting
|
||||
return (
|
||||
<For each={props.topLevelComments}>
|
||||
{(topLevelComment) => (
|
||||
|
||||
@@ -27,7 +27,6 @@ export default function CommentSortingSelect(props: CommentSortingSelectProps) {
|
||||
props.setSorting(mode);
|
||||
setIsOpen(false);
|
||||
|
||||
// Update URL with sortBy parameter
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("sortBy", mode);
|
||||
navigate(`${location.pathname}?${url.searchParams.toString()}#comments`, {
|
||||
|
||||
@@ -18,7 +18,6 @@ export default function DeletePostButton(props: DeletePostButtonProps) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.database.deletePost.mutate({ id: props.postID });
|
||||
// Refresh the page after successful deletion
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
alert("Failed to delete post");
|
||||
|
||||
@@ -32,14 +32,12 @@ export default function MermaidRenderer() {
|
||||
const id = `mermaid-${index}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { svg } = await mermaid.render(id, content);
|
||||
|
||||
// Replace the pre/code with rendered SVG
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "mermaid-rendered";
|
||||
wrapper.innerHTML = svg;
|
||||
pre.replaceWith(wrapper);
|
||||
} catch (err) {
|
||||
console.error("Failed to render mermaid diagram:", err);
|
||||
// Keep the original code block if rendering fails
|
||||
pre.classList.add("mermaid-error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,10 +105,8 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
const pre = codeBlock.parentElement;
|
||||
if (!pre) return;
|
||||
|
||||
// Skip mermaid diagrams
|
||||
if (pre.dataset.type === "mermaid") return;
|
||||
|
||||
// Check if already processed (has header with copy button)
|
||||
const existingHeader = pre.previousElementSibling;
|
||||
if (
|
||||
existingHeader?.classList.contains("language-header") &&
|
||||
@@ -117,53 +115,43 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set off-black background for code block
|
||||
pre.style.backgroundColor = "#1a1a1a";
|
||||
|
||||
// Extract language from code block classes
|
||||
const classes = Array.from(codeBlock.classList);
|
||||
const languageClass = classes.find((cls) => cls.startsWith("language-"));
|
||||
const language = languageClass?.replace("language-", "") || "";
|
||||
|
||||
// Create language header if language is detected
|
||||
if (language) {
|
||||
const languageHeader = document.createElement("div");
|
||||
languageHeader.className = "language-header";
|
||||
languageHeader.style.backgroundColor = "#1a1a1a";
|
||||
|
||||
// Add language label
|
||||
const languageLabel = document.createElement("span");
|
||||
languageLabel.textContent = language;
|
||||
languageHeader.appendChild(languageLabel);
|
||||
|
||||
// Create copy button in header
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.className = "copy-button";
|
||||
copyButton.textContent = "Copy";
|
||||
copyButton.dataset.codeBlock = "true";
|
||||
|
||||
// Store reference to the code block for copying
|
||||
copyButton.dataset.codeBlockId = `code-${Math.random().toString(36).substr(2, 9)}`;
|
||||
codeBlock.dataset.codeBlockId = copyButton.dataset.codeBlockId;
|
||||
|
||||
languageHeader.appendChild(copyButton);
|
||||
|
||||
// Insert header before pre element
|
||||
pre.parentElement?.insertBefore(languageHeader, pre);
|
||||
}
|
||||
|
||||
// Add line numbers
|
||||
const codeText = codeBlock.textContent || "";
|
||||
const lines = codeText.split("\n");
|
||||
const lineCount =
|
||||
lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
||||
|
||||
if (lineCount > 0 && !pre.querySelector(".line-numbers")) {
|
||||
// Create line numbers container
|
||||
const lineNumbers = document.createElement("div");
|
||||
lineNumbers.className = "line-numbers";
|
||||
|
||||
// Generate line numbers
|
||||
for (let i = 1; i <= lineCount; i++) {
|
||||
const lineNum = document.createElement("div");
|
||||
lineNum.textContent = i.toString();
|
||||
@@ -214,7 +202,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
}
|
||||
});
|
||||
|
||||
// Look for the references section marker to get the custom heading name
|
||||
const marker = contentRef.querySelector(
|
||||
"span[id='references-section-start']"
|
||||
) as HTMLElement | null;
|
||||
@@ -233,13 +220,11 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
if (referencesSection) {
|
||||
referencesSection.className = "text-2xl font-bold mb-4 text-text";
|
||||
|
||||
// Find the parent container and add styling
|
||||
const parentDiv = referencesSection.parentElement;
|
||||
if (parentDiv) {
|
||||
parentDiv.classList.add("references-heading");
|
||||
}
|
||||
|
||||
// Find all paragraphs after the References heading that start with [n]
|
||||
let currentElement = referencesSection.nextElementSibling;
|
||||
|
||||
while (currentElement) {
|
||||
@@ -251,29 +236,22 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
const refNumber = match[1];
|
||||
const refId = `ref-${refNumber}`;
|
||||
|
||||
// Set the ID for linking
|
||||
currentElement.id = refId;
|
||||
|
||||
// Add styling
|
||||
currentElement.className =
|
||||
"reference-item transition-colors duration-500 text-sm mb-3";
|
||||
|
||||
// Parse and style the content - get everything after [n]
|
||||
let refText = text.substring(match[0].length);
|
||||
|
||||
// Remove any existing "↑ Back" text (including various Unicode arrow variants)
|
||||
refText = refText.replace(/[↑⬆️]\s*Back\s*$/i, "").trim();
|
||||
|
||||
// Create styled content
|
||||
currentElement.innerHTML = "";
|
||||
|
||||
// Add bold reference number
|
||||
const refNumSpan = document.createElement("span");
|
||||
refNumSpan.className = "text-blue font-semibold";
|
||||
refNumSpan.textContent = `[${refNumber}]`;
|
||||
currentElement.appendChild(refNumSpan);
|
||||
|
||||
// Add reference text
|
||||
if (refText) {
|
||||
const refTextSpan = document.createElement("span");
|
||||
refTextSpan.className = "ml-2";
|
||||
@@ -286,7 +264,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
currentElement.appendChild(refTextSpan);
|
||||
}
|
||||
|
||||
// Add back button
|
||||
const backLink = document.createElement("a");
|
||||
backLink.href = `#ref-${refNumber}-back`;
|
||||
backLink.className =
|
||||
@@ -297,7 +274,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
const target = document.getElementById(`ref-${refNumber}-back`);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
// Highlight the reference link briefly
|
||||
target.style.backgroundColor = "rgba(203, 166, 247, 0.2)";
|
||||
setTimeout(() => {
|
||||
target.style.backgroundColor = "";
|
||||
@@ -308,7 +284,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've reached another heading (end of references)
|
||||
if (
|
||||
currentElement.tagName.match(/^H[1-6]$/) &&
|
||||
currentElement !== referencesSection
|
||||
@@ -321,14 +296,12 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Load highlight.js only when needed
|
||||
createEffect(() => {
|
||||
if (props.hasCodeBlock && !hljs()) {
|
||||
loadHighlightJS().then(setHljs);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply syntax highlighting when hljs loads and when body changes
|
||||
createEffect(() => {
|
||||
const hljsInstance = hljs();
|
||||
if (hljsInstance && props.hasCodeBlock && contentRef) {
|
||||
@@ -339,7 +312,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
}
|
||||
});
|
||||
|
||||
// Process references after content is mounted and when body changes
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
processReferences();
|
||||
@@ -348,14 +320,11 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
}
|
||||
}, 150);
|
||||
|
||||
// Event delegation for copy buttons (single listener for all buttons)
|
||||
if (contentRef) {
|
||||
const handleCopyButtonInteraction = async (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Handle click
|
||||
if (e.type === "click" && target.classList.contains("copy-button")) {
|
||||
// Find the code block using the stored ID
|
||||
const codeBlockId = target.dataset.codeBlockId;
|
||||
const codeBlock = codeBlockId
|
||||
? contentRef?.querySelector(
|
||||
@@ -389,13 +358,11 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Single event listener for all copy button interactions
|
||||
contentRef.addEventListener("click", handleCopyButtonInteraction);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
// Re-process when body changes
|
||||
if (props.body && contentRef) {
|
||||
setTimeout(() => {
|
||||
processReferences();
|
||||
|
||||
@@ -21,7 +21,6 @@ export default function PostSorting(props: PostSortingProps) {
|
||||
const filteredPosts = createMemo(() => {
|
||||
let filtered = props.posts;
|
||||
|
||||
// Apply publication status filter (admin only)
|
||||
if (props.privilegeLevel === "admin" && props.status) {
|
||||
if (props.status === "published") {
|
||||
filtered = filtered.filter((post) => post.published === 1);
|
||||
@@ -30,24 +29,20 @@ export default function PostSorting(props: PostSortingProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Build map of post_id -> tags for that post
|
||||
const postTags = new Map<number, Set<string>>();
|
||||
props.tags.forEach((tag) => {
|
||||
if (!postTags.has(tag.post_id)) {
|
||||
postTags.set(tag.post_id, new Set());
|
||||
}
|
||||
// Tag values in DB have # prefix, remove it for comparison
|
||||
const tagWithoutHash = tag.value.startsWith("#")
|
||||
? tag.value.slice(1)
|
||||
: tag.value;
|
||||
postTags.get(tag.post_id)!.add(tagWithoutHash);
|
||||
});
|
||||
|
||||
// WHITELIST MODE: Only show posts that have at least one of the included tags
|
||||
if (props.include !== undefined) {
|
||||
const includeList = props.include.split("|").filter(Boolean);
|
||||
|
||||
// Empty whitelist means show nothing
|
||||
if (includeList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -58,7 +53,6 @@ export default function PostSorting(props: PostSortingProps) {
|
||||
const tags = postTags.get(post.id);
|
||||
if (!tags || tags.size === 0) return false;
|
||||
|
||||
// Post must have at least one tag from the include list
|
||||
for (const tag of tags) {
|
||||
if (includeSet.has(tag)) {
|
||||
return true;
|
||||
@@ -68,11 +62,9 @@ export default function PostSorting(props: PostSortingProps) {
|
||||
});
|
||||
}
|
||||
|
||||
// BLACKLIST MODE: Hide posts that have ANY of the filtered tags
|
||||
if (props.filters !== undefined) {
|
||||
const filterList = props.filters.split("|").filter(Boolean);
|
||||
|
||||
// Empty blacklist means show everything
|
||||
if (filterList.length === 0) {
|
||||
return filtered;
|
||||
}
|
||||
@@ -81,19 +73,17 @@ export default function PostSorting(props: PostSortingProps) {
|
||||
|
||||
return filtered.filter((post) => {
|
||||
const tags = postTags.get(post.id);
|
||||
if (!tags || tags.size === 0) return true; // Show posts with no tags
|
||||
if (!tags || tags.size === 0) return true;
|
||||
|
||||
// Post must NOT have any blacklisted tags
|
||||
for (const tag of tags) {
|
||||
if (filterSet.has(tag)) {
|
||||
return false; // Hide this post
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true; // Show this post
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// No filters: show all posts
|
||||
return filtered;
|
||||
});
|
||||
|
||||
@@ -105,7 +95,6 @@ export default function PostSorting(props: PostSortingProps) {
|
||||
sorted.reverse(); // Posts come oldest first from DB
|
||||
break;
|
||||
case "oldest":
|
||||
// Already in oldest order from DB
|
||||
break;
|
||||
case "most_liked":
|
||||
sorted.sort((a, b) => (b.total_likes || 0) - (a.total_likes || 0));
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function PostSortingSelect(props: PostSortingSelectProps) {
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Derive selected from URL params instead of local state
|
||||
const selected = () => {
|
||||
const sortParam = searchParams.sort || "newest";
|
||||
return sorting.find((s) => s.val === sortParam) || sorting[0];
|
||||
@@ -28,7 +27,6 @@ export default function PostSortingSelect(props: PostSortingSelectProps) {
|
||||
const handleSelect = (sort: { val: string; label: string }) => {
|
||||
setIsOpen(false);
|
||||
|
||||
// Build new URL preserving all existing params
|
||||
const params = new URLSearchParams(searchParams as Record<string, string>);
|
||||
params.set("sort", sort.val);
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
const currentInclude = () =>
|
||||
searchParams.include?.split("|").filter(Boolean) || [];
|
||||
|
||||
// Get currently selected tags based on mode
|
||||
const selectedTags = () => {
|
||||
if (filterMode() === "whitelist") {
|
||||
return currentInclude();
|
||||
@@ -38,14 +37,12 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Sync filter mode with URL params and ensure one is always present
|
||||
createEffect(() => {
|
||||
if ("include" in searchParams) {
|
||||
setFilterMode("whitelist");
|
||||
} else if ("filter" in searchParams) {
|
||||
setFilterMode("blacklist");
|
||||
} else {
|
||||
// No filter param exists, default to blacklist mode with empty filter
|
||||
const params = new URLSearchParams(
|
||||
searchParams as Record<string, string>
|
||||
);
|
||||
@@ -66,7 +63,6 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
Object.keys(props.tagMap).map((key) => key.slice(1))
|
||||
);
|
||||
|
||||
// Check if a tag is currently selected
|
||||
const isTagChecked = (tag: string) => {
|
||||
return selectedTags().includes(tag);
|
||||
};
|
||||
@@ -106,26 +102,21 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
let newSelected: string[];
|
||||
|
||||
if (isChecked) {
|
||||
// Add tag to selection
|
||||
newSelected = [...currentSelected, tag];
|
||||
} else {
|
||||
// Remove tag from selection
|
||||
newSelected = currentSelected.filter((t) => t !== tag);
|
||||
}
|
||||
|
||||
// Build URL preserving all existing params
|
||||
const params = new URLSearchParams(searchParams as Record<string, string>);
|
||||
const paramName = filterMode() === "whitelist" ? "include" : "filter";
|
||||
const otherParamName = filterMode() === "whitelist" ? "filter" : "include";
|
||||
|
||||
// Remove the other mode's param
|
||||
params.delete(otherParamName);
|
||||
|
||||
if (newSelected.length > 0) {
|
||||
const paramValue = newSelected.join("|");
|
||||
params.set(paramName, paramValue);
|
||||
} else {
|
||||
// Keep empty param to preserve mode (especially important for whitelist)
|
||||
params.set(paramName, "");
|
||||
}
|
||||
|
||||
@@ -137,14 +128,11 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
const paramName = filterMode() === "whitelist" ? "include" : "filter";
|
||||
const otherParamName = filterMode() === "whitelist" ? "filter" : "include";
|
||||
|
||||
// Remove the other mode's param
|
||||
params.delete(otherParamName);
|
||||
|
||||
if (allChecked()) {
|
||||
// Uncheck all: keep empty param to preserve mode
|
||||
params.set(paramName, "");
|
||||
} else {
|
||||
// Check all: select all tags
|
||||
const allTags = allTagKeys().join("|");
|
||||
params.set(paramName, allTags);
|
||||
}
|
||||
@@ -153,7 +141,6 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
};
|
||||
|
||||
const toggleFilterMode = () => {
|
||||
// Get current tags BEFORE changing mode
|
||||
const currentSelected = selectedTags();
|
||||
|
||||
const newMode = filterMode() === "whitelist" ? "blacklist" : "whitelist";
|
||||
@@ -164,14 +151,12 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
const newParamName = newMode === "whitelist" ? "include" : "filter";
|
||||
const oldParamName = newMode === "whitelist" ? "filter" : "include";
|
||||
|
||||
// Remove old param and set new one
|
||||
params.delete(oldParamName);
|
||||
|
||||
if (currentSelected.length > 0) {
|
||||
const paramValue = currentSelected.join("|");
|
||||
params.set(newParamName, paramValue);
|
||||
} else {
|
||||
// Always keep the param, even if empty
|
||||
params.set(newParamName, "");
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,11 @@ export const Mermaid = Node.create({
|
||||
content: {
|
||||
default: "",
|
||||
parseHTML: (element) => {
|
||||
// Try to get code element
|
||||
const code = element.querySelector("code");
|
||||
if (code) {
|
||||
// Get text content, which strips out all HTML tags (including spans from syntax highlighting)
|
||||
return code.textContent || "";
|
||||
}
|
||||
// Fallback to element's own text content
|
||||
return element.textContent || "";
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
@@ -191,15 +189,12 @@ export const Mermaid = Node.create({
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import to avoid bundling issues
|
||||
const mermaid = (await import("mermaid")).default;
|
||||
await mermaid.parse(content);
|
||||
// Valid - green indicator
|
||||
statusIndicator.className =
|
||||
"absolute top-2 left-2 w-3 h-3 rounded-full bg-green opacity-0 group-hover:opacity-100 transition-opacity duration-200";
|
||||
statusIndicator.title = "Valid mermaid syntax";
|
||||
} catch (err) {
|
||||
// Invalid - red indicator
|
||||
statusIndicator.className =
|
||||
"absolute top-2 left-2 w-3 h-3 rounded-full bg-red opacity-0 group-hover:opacity-100 transition-opacity duration-200";
|
||||
statusIndicator.title = `Invalid syntax: ${
|
||||
@@ -208,7 +203,6 @@ export const Mermaid = Node.create({
|
||||
}
|
||||
};
|
||||
|
||||
// Run validation
|
||||
validateSyntax();
|
||||
|
||||
// Edit button overlay - visible on mobile tap/selection, hover on desktop
|
||||
@@ -240,22 +234,18 @@ export const Mermaid = Node.create({
|
||||
const pos = getPos();
|
||||
const { from, to } = editor.state.selection;
|
||||
|
||||
// Check if this node is selected
|
||||
const nodeIsSelected = from === pos && to === pos + node.nodeSize;
|
||||
|
||||
if (nodeIsSelected !== isSelected) {
|
||||
isSelected = nodeIsSelected;
|
||||
if (isSelected) {
|
||||
// Show button when selected (for mobile)
|
||||
editBtn.style.opacity = "1";
|
||||
} else {
|
||||
// Hide button when not selected (reset to CSS control)
|
||||
editBtn.style.opacity = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for selection changes
|
||||
const plugin = editor.view.state.plugins.find(
|
||||
(p: any) => p.spec?.key === "mermaidSelection"
|
||||
);
|
||||
@@ -266,10 +256,8 @@ export const Mermaid = Node.create({
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
// Check selection periodically when visible
|
||||
updateInterval = setInterval(updateButtonVisibility, 100);
|
||||
} else {
|
||||
// Stop checking when not visible
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
updateInterval = null;
|
||||
@@ -282,7 +270,6 @@ export const Mermaid = Node.create({
|
||||
|
||||
observer.observe(dom);
|
||||
|
||||
// Also check on touch
|
||||
dom.addEventListener("touchstart", () => {
|
||||
setTimeout(updateButtonVisibility, 50);
|
||||
});
|
||||
@@ -299,7 +286,6 @@ export const Mermaid = Node.create({
|
||||
return false;
|
||||
}
|
||||
code.textContent = updatedNode.attrs.content || "";
|
||||
// Re-validate on update
|
||||
validateSyntax();
|
||||
updateButtonVisibility();
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user