Refactor components for performance and accessibility improvements
- Removed unused Lucide CDN script from BaseHead component to reduce network costs. - Enhanced CustomCursor component with requestAnimationFrame for smoother animations and respect for user motion preferences. - Updated Hero section styles for faster fade-out transitions and improved responsiveness to user preferences. - Optimized clock functionality in Hero section to reduce drift and improve performance. - Streamlined mousemove event handling in Hero section for better performance and reduced resource usage. - Lazy-loaded markdown renderer in contact page to keep initial JavaScript lighter. - Added will-change property to global CSS for improved rendering performance.
This commit is contained in:
parent
43d52e452d
commit
17c2a3603f
@ -137,7 +137,7 @@ const professionalServiceSchema = {
|
|||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<!-- Icons - Load async to prevent render blocking -->
|
<!-- Icons - Load async to prevent render blocking -->
|
||||||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
<!-- (Removed) Lucide CDN script: currently unused in this repo and adds a global network+JS cost. -->
|
||||||
|
|
||||||
<!-- Canonical URL -->
|
<!-- Canonical URL -->
|
||||||
<link rel="canonical" href={canonicalURL} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
|||||||
@ -10,25 +10,78 @@ const CustomCursor = () => {
|
|||||||
|
|
||||||
if (!dot || !outline) return;
|
if (!dot || !outline) return;
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
// Respect user preferences
|
||||||
const posX = e.clientX;
|
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
|
||||||
const posY = e.clientY;
|
if (reduceMotion) return;
|
||||||
|
|
||||||
// Dot follows instantly
|
// rAF-driven cursor animation (single loop, no per-event Web Animations allocations)
|
||||||
dot.style.left = `${posX}px`;
|
let targetX = 0;
|
||||||
dot.style.top = `${posY}px`;
|
let targetY = 0;
|
||||||
|
let dotX = 0;
|
||||||
|
let dotY = 0;
|
||||||
|
let outlineX = 0;
|
||||||
|
let outlineY = 0;
|
||||||
|
|
||||||
// Outline follows with animation
|
let hasInit = false;
|
||||||
outline.animate({
|
let rafId: number | null = null;
|
||||||
left: `${posX}px`,
|
let lastMoveTs = performance.now();
|
||||||
top: `${posY}px`
|
|
||||||
}, { duration: 500, fill: "forwards" });
|
const DOT_LERP = 0.65; // tighter = closer to 1
|
||||||
|
const OUTLINE_LERP = 0.18; // tighter = closer to 1
|
||||||
|
const STOP_AFTER_MS = 140;
|
||||||
|
const STOP_EPS_PX = 0.35;
|
||||||
|
|
||||||
|
const applyTransforms = () => {
|
||||||
|
dot.style.transform = `translate3d(${dotX}px, ${dotY}px, 0) translate(-50%, -50%)`;
|
||||||
|
outline.style.transform = `translate3d(${outlineX}px, ${outlineY}px, 0) translate(-50%, -50%)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('mousemove', onMouseMove);
|
const tick = (ts: number) => {
|
||||||
|
// Lerp towards the target
|
||||||
|
dotX += (targetX - dotX) * DOT_LERP;
|
||||||
|
dotY += (targetY - dotY) * DOT_LERP;
|
||||||
|
outlineX += (targetX - outlineX) * OUTLINE_LERP;
|
||||||
|
outlineY += (targetY - outlineY) * OUTLINE_LERP;
|
||||||
|
|
||||||
|
applyTransforms();
|
||||||
|
|
||||||
|
const idle = ts - lastMoveTs > STOP_AFTER_MS;
|
||||||
|
const dx = Math.abs(targetX - outlineX);
|
||||||
|
const dy = Math.abs(targetY - outlineY);
|
||||||
|
const settled = dx < STOP_EPS_PX && dy < STOP_EPS_PX;
|
||||||
|
|
||||||
|
if (idle && settled) {
|
||||||
|
rafId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId = window.requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
targetX = e.clientX;
|
||||||
|
targetY = e.clientY;
|
||||||
|
lastMoveTs = performance.now();
|
||||||
|
|
||||||
|
if (!hasInit) {
|
||||||
|
hasInit = true;
|
||||||
|
dotX = targetX;
|
||||||
|
dotY = targetY;
|
||||||
|
outlineX = targetX;
|
||||||
|
outlineY = targetY;
|
||||||
|
applyTransforms();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rafId === null) {
|
||||||
|
rafId = window.requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', onMouseMove, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('mousemove', onMouseMove);
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
if (rafId !== null) window.cancelAnimationFrame(rafId);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,8 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
}
|
}
|
||||||
/* Fade out */
|
/* Fade out */
|
||||||
.grid-cell {
|
.grid-cell {
|
||||||
transition: opacity 0.8s ease-out, background-color 0.8s ease-out;
|
/* Slightly faster fade-out for a snappier feel */
|
||||||
|
transition: opacity 0.6s ease-out, background-color 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initial Loaded State Classes */
|
/* Initial Loaded State Classes */
|
||||||
@ -112,17 +113,40 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Clock
|
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false;
|
||||||
function updateClock() {
|
const finePointer = window.matchMedia?.('(pointer: fine) and (hover: hover)')?.matches ?? false;
|
||||||
|
|
||||||
|
// ===== CLOCK (pause on hidden tab, align to second boundaries) =====
|
||||||
|
let clockTimer = 0;
|
||||||
|
|
||||||
|
function updateClockOnce() {
|
||||||
const clock = document.getElementById('clock');
|
const clock = document.getElementById('clock');
|
||||||
if (clock) {
|
if (!clock) return;
|
||||||
const now = new Date();
|
|
||||||
const timeString = now.toLocaleTimeString('en-US', {hour12: false, timeZone: 'America/Denver'});
|
const now = new Date();
|
||||||
clock.textContent = timeString + " MST";
|
const timeString = now.toLocaleTimeString('en-US', { hour12: false, timeZone: 'America/Denver' });
|
||||||
}
|
clock.textContent = `${timeString} MST`;
|
||||||
}
|
}
|
||||||
setInterval(updateClock, 1000);
|
|
||||||
updateClock();
|
function startClock() {
|
||||||
|
if (clockTimer) window.clearTimeout(clockTimer);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
clockTimer = window.setTimeout(tick, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClockOnce();
|
||||||
|
// Align to the next second boundary to reduce drift.
|
||||||
|
const msToNextSecond = 1000 - (Date.now() % 1000);
|
||||||
|
clockTimer = window.setTimeout(tick, msToNextSecond);
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
startClock();
|
||||||
|
|
||||||
// Intro Animation Sequence
|
// Intro Animation Sequence
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
@ -138,21 +162,23 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
portrait.classList.add('portrait-visible');
|
portrait.classList.add('portrait-visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger Grid Ripple
|
// Trigger Grid Ripple (skip if reduced motion)
|
||||||
const cells = document.querySelectorAll('.grid-cell');
|
if (!reduceMotion) {
|
||||||
// Diagonal sweep effect
|
const cells = document.querySelectorAll('.grid-cell');
|
||||||
cells.forEach((cell, i) => {
|
// Diagonal sweep effect
|
||||||
const row = Math.floor(i / 10);
|
cells.forEach((cell, i) => {
|
||||||
const col = i % 10;
|
const row = Math.floor(i / 10);
|
||||||
const delay = (row + col) * 50; // Diagonal delay
|
const col = i % 10;
|
||||||
|
const delay = (row + col) * 45; // slightly faster diagonal delay
|
||||||
|
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cell.classList.add('active');
|
cell.classList.add('active');
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cell.classList.remove('active');
|
cell.classList.remove('active');
|
||||||
}, 200);
|
}, 180);
|
||||||
}, delay);
|
}, delay);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -161,67 +187,85 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
const cells = document.querySelectorAll('.grid-cell');
|
const cells = document.querySelectorAll('.grid-cell');
|
||||||
|
|
||||||
if (section) {
|
if (section) {
|
||||||
section.addEventListener('mousemove', (e) => {
|
// Throttle mousemove work to one update per frame.
|
||||||
const rect = section.getBoundingClientRect();
|
let latestX = 0;
|
||||||
// Calculate relative coordinates
|
let latestY = 0;
|
||||||
const x = e.clientX - rect.left;
|
let pending = false;
|
||||||
const y = e.clientY - rect.top;
|
let lastIndex = -1;
|
||||||
|
const timeouts: number[] = new Array(cells.length).fill(0);
|
||||||
|
|
||||||
// Calculate grid dimensions
|
const process = () => {
|
||||||
|
pending = false;
|
||||||
|
if (!finePointer || reduceMotion) return;
|
||||||
|
|
||||||
|
const rect = section.getBoundingClientRect();
|
||||||
const width = rect.width;
|
const width = rect.width;
|
||||||
const height = rect.height;
|
const height = rect.height;
|
||||||
|
if (width <= 0 || height <= 0) return;
|
||||||
|
|
||||||
|
const x = latestX - rect.left;
|
||||||
|
const y = latestY - rect.top;
|
||||||
|
|
||||||
// 10x10 grid
|
|
||||||
const col = Math.floor((x / width) * 10);
|
const col = Math.floor((x / width) * 10);
|
||||||
const row = Math.floor((y / height) * 10);
|
const row = Math.floor((y / height) * 10);
|
||||||
|
|
||||||
// Bounds check
|
if (col < 0 || col >= 10 || row < 0 || row >= 10) return;
|
||||||
if (col >= 0 && col < 10 && row >= 0 && row < 10) {
|
|
||||||
const index = row * 10 + col;
|
|
||||||
const cell = cells[index];
|
|
||||||
|
|
||||||
if (cell) {
|
const index = row * 10 + col;
|
||||||
// Remove active class from all others (optional, or let them fade)
|
if (index === lastIndex) return;
|
||||||
// cell.classList.add('active');
|
lastIndex = index;
|
||||||
|
|
||||||
// To prevent "sticking", we can use a timestamp or just let CSS transition handle fade out
|
const cell = cells[index] as HTMLElement | undefined;
|
||||||
// But we need to trigger the "hit"
|
if (!cell) return;
|
||||||
|
|
||||||
// Logic: Add active, then remove it shortly after to trigger fade
|
cell.classList.add('active');
|
||||||
cell.classList.add('active');
|
|
||||||
|
|
||||||
// Clear previous timeout if this cell was just hit?
|
const prev = timeouts[index];
|
||||||
// Simpler: Just add active. Use requestAnimationFrame to clear "old" active cells?
|
if (prev) window.clearTimeout(prev);
|
||||||
// Or just let the CSS transition handle the fade out when class is removed.
|
|
||||||
|
|
||||||
// Better approach for "trail":
|
// Shorter hold time for a quicker trail.
|
||||||
// Add active class. Set a timeout to remove it.
|
timeouts[index] = window.setTimeout(() => {
|
||||||
|
cell.classList.remove('active');
|
||||||
|
timeouts[index] = 0;
|
||||||
|
}, 35);
|
||||||
|
};
|
||||||
|
|
||||||
if (cell.dataset.timeout) {
|
section.addEventListener('mousemove', (e) => {
|
||||||
clearTimeout(Number(cell.dataset.timeout));
|
latestX = e.clientX;
|
||||||
}
|
latestY = e.clientY;
|
||||||
|
if (pending) return;
|
||||||
const timeoutId = setTimeout(() => {
|
pending = true;
|
||||||
cell.classList.remove('active');
|
window.requestAnimationFrame(process);
|
||||||
}, 50); // Short "hold" time, then fade out via CSS
|
}, { passive: true });
|
||||||
|
|
||||||
cell.dataset.timeout = String(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random pulse for liveliness
|
// Random pulse for liveliness
|
||||||
function pulseRandomSquare() {
|
let pulseInterval = 0;
|
||||||
// Only pulse if not currently interacting (optional, but keeps it cleaner)
|
|
||||||
const randomIndex = Math.floor(Math.random() * cells.length);
|
function startPulse() {
|
||||||
const cell = cells[randomIndex];
|
if (pulseInterval) window.clearInterval(pulseInterval);
|
||||||
if(cell) {
|
if (!finePointer || reduceMotion) return;
|
||||||
|
|
||||||
|
pulseInterval = window.setInterval(() => {
|
||||||
|
if (document.hidden) return;
|
||||||
|
|
||||||
|
const randomIndex = Math.floor(Math.random() * cells.length);
|
||||||
|
const cell = cells[randomIndex] as HTMLElement | undefined;
|
||||||
|
if (!cell) return;
|
||||||
|
|
||||||
cell.classList.add('active');
|
cell.classList.add('active');
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cell.classList.remove('active');
|
cell.classList.remove('active');
|
||||||
}, 200);
|
}, 160);
|
||||||
}
|
}, 1200);
|
||||||
}
|
}
|
||||||
setInterval(pulseRandomSquare, 1500);
|
|
||||||
|
startPulse();
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
// Keep timers light in background.
|
||||||
|
if (!document.hidden) {
|
||||||
|
updateClockOnce();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -42,7 +42,8 @@ const {
|
|||||||
<slot name="head" />
|
<slot name="head" />
|
||||||
</head>
|
</head>
|
||||||
<body class="antialiased selection:bg-brand-accent selection:text-brand-dark bg-brand-dark text-white">
|
<body class="antialiased selection:bg-brand-accent selection:text-brand-dark bg-brand-dark text-white">
|
||||||
<CustomCursor client:load />
|
<!-- Only hydrate custom cursor on devices that can actually benefit from it -->
|
||||||
|
<CustomCursor client:media="(pointer: fine) and (hover: hover)" />
|
||||||
<GridOverlay />
|
<GridOverlay />
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
@ -53,14 +54,9 @@ const {
|
|||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Initialize Lucide icons
|
|
||||||
// @ts-ignore
|
|
||||||
if (window.lucide) {
|
|
||||||
// @ts-ignore
|
|
||||||
window.lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== SCROLL ANIMATION SYSTEM =====
|
// ===== SCROLL ANIMATION SYSTEM =====
|
||||||
|
// If you're using Astro view transitions, elements can change between navigations.
|
||||||
|
// We'll (re)bind observers on initial load and on `astro:page-load`.
|
||||||
|
|
||||||
// Observer for scroll-triggered animations
|
// Observer for scroll-triggered animations
|
||||||
const scrollObserverOptions = {
|
const scrollObserverOptions = {
|
||||||
@ -78,11 +74,6 @@ const {
|
|||||||
});
|
});
|
||||||
}, scrollObserverOptions);
|
}, scrollObserverOptions);
|
||||||
|
|
||||||
// Observe all animate-on-scroll elements
|
|
||||||
document.querySelectorAll('.animate-on-scroll').forEach(el => {
|
|
||||||
scrollObserver.observe(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Observer for legacy reveal-text animations
|
// Observer for legacy reveal-text animations
|
||||||
const revealObserverOptions = {
|
const revealObserverOptions = {
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
@ -97,17 +88,93 @@ const {
|
|||||||
});
|
});
|
||||||
}, revealObserverOptions);
|
}, revealObserverOptions);
|
||||||
|
|
||||||
document.querySelectorAll('.reveal-text').forEach(el => {
|
function bindScrollAnimations() {
|
||||||
revealObserver.observe(el);
|
// Observe animate-on-scroll elements (avoid re-observing elements already visible)
|
||||||
});
|
document.querySelectorAll('.animate-on-scroll:not(.is-visible)').forEach(el => {
|
||||||
|
scrollObserver.observe(el);
|
||||||
// Auto-stagger children in containers with .stagger-children class
|
|
||||||
document.querySelectorAll('.stagger-children').forEach(container => {
|
|
||||||
const children = container.querySelectorAll('.animate-on-scroll');
|
|
||||||
children.forEach((child, index) => {
|
|
||||||
child.classList.add(`stagger-${Math.min(index + 1, 8)}`);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// Observe reveal-text elements
|
||||||
|
document.querySelectorAll('.reveal-text:not(.active)').forEach(el => {
|
||||||
|
revealObserver.observe(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-stagger children in containers with .stagger-children class
|
||||||
|
document.querySelectorAll('.stagger-children').forEach(container => {
|
||||||
|
const children = container.querySelectorAll('.animate-on-scroll');
|
||||||
|
children.forEach((child, index) => {
|
||||||
|
child.classList.add(`stagger-${Math.min(index + 1, 8)}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial bind
|
||||||
|
bindScrollAnimations();
|
||||||
|
|
||||||
|
// Re-bind on Astro page transitions (if enabled)
|
||||||
|
document.addEventListener('astro:page-load', bindScrollAnimations);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ===== INTENT-BASED PREFETCH (hover/focus) =====
|
||||||
|
// Lightweight prefetch to make navigation feel instant without a full SPA router.
|
||||||
|
const prefetched = new Set();
|
||||||
|
|
||||||
|
function isPrefetchableUrl(url) {
|
||||||
|
try {
|
||||||
|
const u = new URL(url, window.location.href);
|
||||||
|
if (u.origin !== window.location.origin) return false;
|
||||||
|
if (u.hash) return false;
|
||||||
|
if (u.pathname === window.location.pathname && u.search === window.location.search) return false;
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchDocument(url) {
|
||||||
|
if (!isPrefetchableUrl(url)) return;
|
||||||
|
const u = new URL(url, window.location.href);
|
||||||
|
const key = u.href;
|
||||||
|
if (prefetched.has(key)) return;
|
||||||
|
prefetched.add(key);
|
||||||
|
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'prefetch';
|
||||||
|
link.as = 'document';
|
||||||
|
link.href = key;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnchorFromEventTarget(target) {
|
||||||
|
if (!(target instanceof Element)) return null;
|
||||||
|
return target.closest('a[href]');
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = (href) => {
|
||||||
|
// Don't block input; prefetch when the browser is idle if possible.
|
||||||
|
// @ts-ignore - requestIdleCallback isn't in all TS lib targets
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.requestIdleCallback(() => prefetchDocument(href), { timeout: 1000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(() => prefetchDocument(href), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseover', (e) => {
|
||||||
|
const a = getAnchorFromEventTarget(e.target);
|
||||||
|
const href = a?.getAttribute('href');
|
||||||
|
if (!href) return;
|
||||||
|
schedule(href);
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener('focusin', (e) => {
|
||||||
|
const a = getAnchorFromEventTarget(e.target);
|
||||||
|
const href = a?.getAttribute('href');
|
||||||
|
if (!href) return;
|
||||||
|
schedule(href);
|
||||||
|
}, { passive: true });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -428,8 +428,6 @@ const contactContent = contactEntry.data;
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { marked } from 'marked';
|
|
||||||
|
|
||||||
// ===== Custom Dropdown Logic =====
|
// ===== Custom Dropdown Logic =====
|
||||||
const selectContainer = document.getElementById('custom-select');
|
const selectContainer = document.getElementById('custom-select');
|
||||||
const selectTrigger = document.getElementById('select-trigger');
|
const selectTrigger = document.getElementById('select-trigger');
|
||||||
@ -657,6 +655,8 @@ const contactContent = contactEntry.data;
|
|||||||
// Success path - render markdown response
|
// Success path - render markdown response
|
||||||
if (data.success && data.format === 'mdx' && data.message) {
|
if (data.success && data.format === 'mdx' && data.message) {
|
||||||
try {
|
try {
|
||||||
|
// Lazy-load markdown renderer only when needed (keeps initial JS lighter)
|
||||||
|
const { marked } = await import('marked');
|
||||||
const htmlContent = await marked.parse(data.message);
|
const htmlContent = await marked.parse(data.message);
|
||||||
responseContent.innerHTML = htmlContent;
|
responseContent.innerHTML = htmlContent;
|
||||||
|
|
||||||
@ -669,7 +669,9 @@ const contactContent = contactEntry.data;
|
|||||||
|
|
||||||
} catch (markdownError) {
|
} catch (markdownError) {
|
||||||
console.error('Markdown parsing error:', markdownError);
|
console.error('Markdown parsing error:', markdownError);
|
||||||
throw new Error('Failed to render response');
|
// Fallback: show plain text instead of failing the whole interaction.
|
||||||
|
responseContent.textContent = String(data.message);
|
||||||
|
showResponse();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid response format from server');
|
throw new Error('Invalid response format from server');
|
||||||
|
|||||||
@ -234,6 +234,7 @@ html {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-dot {
|
.cursor-dot {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user