mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 07:06:45 +03:00
Harden accessibility and add resilience improvements
- Add skip-to-content link for keyboard navigation (WCAG 2.4.1) - Add ARIA labels to download buttons and periodic table elements - Convert periodic table from divs to semantic buttons with keyboard support - Add focus-visible styles to all interactive elements - Implement WAI-ARIA tabs pattern for pattern categories with arrow key nav - Add global prefers-reduced-motion support in CSS and JavaScript - Fix touch gesture trapping in split-compare (allows vertical scroll) - Add error states with retry button for API failures - Add underlines to links for non-color identification (WCAG 1.4.1) - Respect reduced motion preference in Lenis smooth scroll and hero canvas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
5d65154dee
commit
ecc4857204
+130
-18
@@ -25,6 +25,14 @@ async function loadContent() {
|
||||
fetch("/api/patterns"),
|
||||
]);
|
||||
|
||||
// Check for HTTP errors
|
||||
if (!commandsRes.ok) {
|
||||
throw new Error(`Commands API failed: ${commandsRes.status}`);
|
||||
}
|
||||
if (!patternsRes.ok) {
|
||||
throw new Error(`Patterns API failed: ${patternsRes.status}`);
|
||||
}
|
||||
|
||||
allCommands = await commandsRes.json();
|
||||
const patternsData = await patternsRes.json();
|
||||
|
||||
@@ -35,6 +43,39 @@ async function loadContent() {
|
||||
renderPatternsWithTabs(patternsData.patterns, patternsData.antipatterns);
|
||||
} catch (error) {
|
||||
console.error("Failed to load content:", error);
|
||||
showLoadError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function showLoadError(error) {
|
||||
// Show error in commands section
|
||||
const commandsGallery = document.querySelector('.commands-gallery');
|
||||
if (commandsGallery) {
|
||||
commandsGallery.innerHTML = `
|
||||
<div class="load-error" role="alert">
|
||||
<div class="load-error-icon" aria-hidden="true">⚠</div>
|
||||
<h3 class="load-error-title">Failed to load commands</h3>
|
||||
<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
|
||||
<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Show error in patterns section
|
||||
const patternsContainer = document.getElementById("patterns-categories");
|
||||
if (patternsContainer) {
|
||||
patternsContainer.innerHTML = `
|
||||
<div class="load-error" role="alert">
|
||||
<div class="load-error-icon" aria-hidden="true">⚠</div>
|
||||
<h3 class="load-error-title">Failed to load patterns</h3>
|
||||
<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
|
||||
<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,27 +89,46 @@ function renderPatternsWithTabs(patterns, antipatterns) {
|
||||
antipatternMap[cat.name] = cat.items;
|
||||
});
|
||||
|
||||
// Build tabs
|
||||
// Generate unique IDs for tabs
|
||||
const tabId = (name) => `pattern-tab-${name.toLowerCase().replace(/\s+/g, '-')}`;
|
||||
const panelId = (name) => `pattern-panel-${name.toLowerCase().replace(/\s+/g, '-')}`;
|
||||
|
||||
// Build tabs with WAI-ARIA attributes
|
||||
const tabsHTML = patterns
|
||||
.map((category, i) => `<button class="pattern-tab${i === 0 ? ' active' : ''}" data-tab="${category.name}">${category.name}</button>`)
|
||||
.map((category, i) => `<button
|
||||
class="pattern-tab${i === 0 ? ' active' : ''}"
|
||||
data-tab="${category.name}"
|
||||
role="tab"
|
||||
id="${tabId(category.name)}"
|
||||
aria-selected="${i === 0 ? 'true' : 'false'}"
|
||||
aria-controls="${panelId(category.name)}"
|
||||
tabindex="${i === 0 ? '0' : '-1'}"
|
||||
>${category.name}</button>`)
|
||||
.join("");
|
||||
|
||||
// Build panels
|
||||
// Build panels with WAI-ARIA attributes
|
||||
const panelsHTML = patterns
|
||||
.map((category, i) => {
|
||||
const antiItems = antipatternMap[category.name] || [];
|
||||
return `
|
||||
<div class="pattern-panel${i === 0 ? ' active' : ''}" data-panel="${category.name}">
|
||||
<div
|
||||
class="pattern-panel${i === 0 ? ' active' : ''}"
|
||||
data-panel="${category.name}"
|
||||
role="tabpanel"
|
||||
id="${panelId(category.name)}"
|
||||
aria-labelledby="${tabId(category.name)}"
|
||||
${i !== 0 ? 'hidden' : ''}
|
||||
>
|
||||
<div class="pattern-columns">
|
||||
<div class="pattern-column pattern-column--anti">
|
||||
<span class="pattern-column-label">Don't</span>
|
||||
<ul class="pattern-list">
|
||||
<span class="pattern-column-label" id="dont-label-${i}">Don't</span>
|
||||
<ul class="pattern-list" aria-labelledby="dont-label-${i}">
|
||||
${antiItems.map((item) => `<li class="pattern-item pattern-item--anti">${item}</li>`).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pattern-column pattern-column--do">
|
||||
<span class="pattern-column-label">Do</span>
|
||||
<ul class="pattern-list">
|
||||
<span class="pattern-column-label" id="do-label-${i}">Do</span>
|
||||
<ul class="pattern-list" aria-labelledby="do-label-${i}">
|
||||
${category.items.map((item) => `<li class="pattern-item pattern-item--do">${item}</li>`).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -79,22 +139,74 @@ function renderPatternsWithTabs(patterns, antipatterns) {
|
||||
.join("");
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="pattern-tabs">${tabsHTML}</div>
|
||||
<div class="pattern-tabs" role="tablist" aria-label="Pattern categories">${tabsHTML}</div>
|
||||
<div class="pattern-panels">${panelsHTML}</div>
|
||||
`;
|
||||
|
||||
const tabs = container.querySelectorAll('.pattern-tab');
|
||||
const panels = container.querySelectorAll('.pattern-panel');
|
||||
|
||||
// Function to switch tabs
|
||||
const switchTab = (newTab) => {
|
||||
const tabName = newTab.dataset.tab;
|
||||
|
||||
// Update ARIA attributes on all tabs
|
||||
tabs.forEach(t => {
|
||||
t.classList.remove('active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
t.setAttribute('tabindex', '-1');
|
||||
});
|
||||
|
||||
// Activate the new tab
|
||||
newTab.classList.add('active');
|
||||
newTab.setAttribute('aria-selected', 'true');
|
||||
newTab.setAttribute('tabindex', '0');
|
||||
newTab.focus();
|
||||
|
||||
// Update panels
|
||||
panels.forEach(p => {
|
||||
p.classList.remove('active');
|
||||
p.setAttribute('hidden', '');
|
||||
});
|
||||
const activePanel = container.querySelector(`[data-panel="${tabName}"]`);
|
||||
activePanel.classList.add('active');
|
||||
activePanel.removeAttribute('hidden');
|
||||
};
|
||||
|
||||
// Tab click handling
|
||||
container.querySelectorAll('.pattern-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const tabName = tab.dataset.tab;
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => switchTab(tab));
|
||||
});
|
||||
|
||||
// Update active tab
|
||||
container.querySelectorAll('.pattern-tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
// Keyboard navigation (Arrow keys, Home, End)
|
||||
tabs.forEach((tab, index) => {
|
||||
tab.addEventListener('keydown', (e) => {
|
||||
let targetIndex = index;
|
||||
|
||||
// Update active panel
|
||||
container.querySelectorAll('.pattern-panel').forEach(p => p.classList.remove('active'));
|
||||
container.querySelector(`[data-panel="${tabName}"]`).classList.add('active');
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
targetIndex = index === 0 ? tabs.length - 1 : index - 1;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
targetIndex = index === tabs.length - 1 ? 0 : index + 1;
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
targetIndex = 0;
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
targetIndex = tabs.length - 1;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
switchTab(tabs[targetIndex]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+142
-2
@@ -82,6 +82,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SKIP LINK (Accessibility)
|
||||
============================================ */
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10000;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
transition: top 0.2s ease;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BASE STYLES
|
||||
============================================ */
|
||||
@@ -132,12 +157,22 @@ h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
transition: color var(--duration-fast) var(--ease-out), text-decoration-color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-accent-hover);
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
/* Remove underline for buttons styled as links */
|
||||
.btn,
|
||||
.footer-logo,
|
||||
[class*="nav-item"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
@@ -1210,6 +1245,21 @@ code {
|
||||
color: var(--color-paper);
|
||||
}
|
||||
|
||||
/* Focus styles for all buttons */
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-primary:focus-visible {
|
||||
outline-color: var(--color-paper);
|
||||
box-shadow: 0 0 0 4px var(--color-accent);
|
||||
}
|
||||
|
||||
.btn-secondary:focus-visible {
|
||||
outline-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ANIMATIONS
|
||||
============================================ */
|
||||
@@ -1255,6 +1305,90 @@ code {
|
||||
[data-reveal]:nth-child(3) { transition-delay: 0.2s; }
|
||||
[data-reveal]:nth-child(4) { transition-delay: 0.3s; }
|
||||
|
||||
/* ============================================
|
||||
REDUCED MOTION (Accessibility)
|
||||
============================================ */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
|
||||
/* Allow smooth scrolling to remain for anchor links only if user explicitly triggers */
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
/* Disable hero canvas animation */
|
||||
.hero-canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Disable scroll indicator animation */
|
||||
.hero-scroll-indicator {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Disable reveal animations - show content immediately */
|
||||
[data-reveal] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Disable gallery frame transitions */
|
||||
.gallery-frame {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ERROR STATES
|
||||
============================================ */
|
||||
|
||||
.load-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xl) var(--spacing-lg);
|
||||
gap: var(--spacing-md);
|
||||
background: var(--color-cream);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.load-error-icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.load-error-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.load-error-text {
|
||||
font-size: 1rem;
|
||||
color: var(--color-ash);
|
||||
max-width: 40ch;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.load-error-retry {
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
IMPORTS
|
||||
============================================ */
|
||||
@@ -1385,6 +1519,12 @@ code {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
.pattern-tab:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.pattern-panel {
|
||||
display: none;
|
||||
|
||||
+13
-10
@@ -22,9 +22,12 @@
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen overflow-x-hidden leading-relaxed">
|
||||
<!-- Skip to main content link for keyboard users -->
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<!-- Grain Overlay -->
|
||||
<div class="grain-overlay" aria-hidden="true"></div>
|
||||
|
||||
|
||||
<!-- 1. HERO - Liquid Typography -->
|
||||
<header class="hero-section" id="hero">
|
||||
<canvas class="hero-canvas" id="hero-canvas"></canvas>
|
||||
@@ -49,7 +52,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="site-content">
|
||||
<main class="site-content" id="main-content">
|
||||
<!-- 2. THE PROBLEM - AI Design Bias -->
|
||||
<section class="problem-section" id="problem">
|
||||
<div class="section-header" data-reveal>
|
||||
@@ -225,9 +228,9 @@
|
||||
<span class="download-card-badge">Rules + Commands</span>
|
||||
<p class="download-card-desc">Commands and rules for Cursor IDE. Works via appending, no frontmatter required.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" data-bundle="cursor">
|
||||
<button class="btn btn-primary" data-bundle="cursor" aria-label="Download Cursor bundle">
|
||||
<span>Download</span>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -244,9 +247,9 @@
|
||||
<span class="download-card-badge">Full Featured</span>
|
||||
<p class="download-card-desc">Complete YAML frontmatter with argument support. Follows Anthropic Skills specification.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" data-bundle="claude-code">
|
||||
<button class="btn btn-primary" data-bundle="claude-code" aria-label="Download Claude Code bundle">
|
||||
<span>Download</span>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -263,9 +266,9 @@
|
||||
<span class="download-card-badge">TOML + Modular</span>
|
||||
<p class="download-card-desc">TOML-formatted commands with modular skill imports via GEMINI.md.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" data-bundle="gemini">
|
||||
<button class="btn btn-primary" data-bundle="gemini" aria-label="Download Gemini CLI bundle">
|
||||
<span>Download</span>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -282,9 +285,9 @@
|
||||
<span class="download-card-badge">Prompts + Routing</span>
|
||||
<p class="download-card-desc">Custom prompt format with skill routing through AGENTS.md architecture.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" data-bundle="codex">
|
||||
<button class="btn btn-primary" data-bundle="codex" aria-label="Download Codex CLI bundle">
|
||||
<span>Download</span>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -261,7 +261,9 @@ export class PeriodicTable {
|
||||
createElement(cmd, category) {
|
||||
const colors = categoryColors[category];
|
||||
|
||||
const el = document.createElement('div');
|
||||
const el = document.createElement('button');
|
||||
el.type = 'button';
|
||||
el.setAttribute('aria-label', `/${cmd} command - ${categoryLabels[category]}`);
|
||||
el.style.cssText = `
|
||||
width: 56px;
|
||||
height: 64px;
|
||||
@@ -275,6 +277,8 @@ export class PeriodicTable {
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
position: relative;
|
||||
font-family: inherit;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
// Atomic number
|
||||
@@ -315,8 +319,8 @@ export class PeriodicTable {
|
||||
name.textContent = `/${cmd}`;
|
||||
el.appendChild(name);
|
||||
|
||||
// Hover effects
|
||||
el.addEventListener('mouseenter', () => {
|
||||
// Shared handler for activation (hover or focus)
|
||||
const activate = () => {
|
||||
// Visual feedback
|
||||
el.style.transform = 'translateY(-2px)';
|
||||
el.style.boxShadow = `0 4px 12px ${colors.border}40`;
|
||||
@@ -325,19 +329,28 @@ export class PeriodicTable {
|
||||
this.showCommandInfo(cmd);
|
||||
|
||||
// Track active element
|
||||
if (this.activeElement) {
|
||||
if (this.activeElement && this.activeElement !== el) {
|
||||
this.activeElement.style.transform = 'translateY(0)';
|
||||
this.activeElement.style.boxShadow = 'none';
|
||||
}
|
||||
this.activeElement = el;
|
||||
});
|
||||
};
|
||||
|
||||
el.addEventListener('mouseleave', () => {
|
||||
// Shared handler for deactivation
|
||||
const deactivate = () => {
|
||||
el.style.transform = 'translateY(0)';
|
||||
el.style.boxShadow = 'none';
|
||||
});
|
||||
};
|
||||
|
||||
// Click to scroll to command
|
||||
// Mouse events
|
||||
el.addEventListener('mouseenter', activate);
|
||||
el.addEventListener('mouseleave', deactivate);
|
||||
|
||||
// Keyboard focus events
|
||||
el.addEventListener('focus', activate);
|
||||
el.addEventListener('blur', deactivate);
|
||||
|
||||
// Click/Enter to scroll to command
|
||||
el.addEventListener('click', () => {
|
||||
const target = document.getElementById(`cmd-${cmd}`);
|
||||
if (target) {
|
||||
|
||||
@@ -2,6 +2,12 @@ export function initHeroEffect() {
|
||||
const canvas = document.getElementById("hero-canvas");
|
||||
if (!canvas) return;
|
||||
|
||||
// Respect user's motion preferences
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
canvas.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
let width, height;
|
||||
let points = [];
|
||||
|
||||
@@ -94,22 +94,52 @@ export function initSplitCompare(container, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart() {
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let isDragging = false;
|
||||
const DRAG_THRESHOLD = 10; // Minimum horizontal movement to start dragging
|
||||
|
||||
function handleTouchStart(e) {
|
||||
const touch = e.touches[0];
|
||||
touchStartX = touch.clientX;
|
||||
touchStartY = touch.clientY;
|
||||
isDragging = false;
|
||||
isHovering = true;
|
||||
}
|
||||
|
||||
function handleTouchEnd() {
|
||||
isHovering = false;
|
||||
isDragging = false;
|
||||
targetX = defaultPosition;
|
||||
startAnimation();
|
||||
}
|
||||
|
||||
function handleTouchMove(e) {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const rect = splitContainer.getBoundingClientRect();
|
||||
targetX = ((touch.clientX - rect.left) / rect.width) * 100;
|
||||
startAnimation();
|
||||
const deltaX = Math.abs(touch.clientX - touchStartX);
|
||||
const deltaY = Math.abs(touch.clientY - touchStartY);
|
||||
|
||||
// Only start dragging if horizontal movement is greater than vertical
|
||||
// This allows vertical scrolling to pass through
|
||||
if (!isDragging) {
|
||||
if (deltaX > DRAG_THRESHOLD && deltaX > deltaY) {
|
||||
isDragging = true;
|
||||
} else if (deltaY > DRAG_THRESHOLD) {
|
||||
// User is scrolling vertically, don't interfere
|
||||
return;
|
||||
} else {
|
||||
// Not enough movement yet
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only prevent default when actively dragging horizontally
|
||||
if (isDragging) {
|
||||
e.preventDefault();
|
||||
const rect = splitContainer.getBoundingClientRect();
|
||||
targetX = ((touch.clientX - rect.left) / rect.width) * 100;
|
||||
startAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
// Attach listeners
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import Lenis from "lenis";
|
||||
|
||||
// Check if user prefers reduced motion
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
export function initSmoothScroll() {
|
||||
// Skip smooth scroll entirely if user prefers reduced motion
|
||||
if (prefersReducedMotion) {
|
||||
// Still handle anchor links but with instant scroll
|
||||
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||
anchor.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(anchor.getAttribute("href"));
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
easing: (t) => Math.min(1, 1.001 - 2 ** (-10 * t)),
|
||||
@@ -28,7 +46,7 @@ export function initSmoothScroll() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return lenis;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user