Skip to content

Commit 9892791

Browse files
committed
Add swipe indicator functionality for scrollable output in PyodideRunner: Implemented a visual cue that appears when the output is scrollable, enhancing user interaction. The indicator fades out after user interaction, and custom scrollbar styles improve visibility and usability across themes.
1 parent 8f9fa2f commit 9892791

1 file changed

Lines changed: 132 additions & 2 deletions

File tree

src/components/PyodideRunner.astro

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,39 @@ const { code, title = "Python Code" } = Astro.props;
148148
});
149149
});
150150

151+
// Add swipe indicator to scrollable output
152+
function checkOutputScrollable(output: Element) {
153+
const outputEl = output as HTMLElement;
154+
// Check if output is scrollable (content wider or taller than container)
155+
const isScrollable = outputEl.scrollWidth > outputEl.clientWidth + 5 ||
156+
outputEl.scrollHeight > outputEl.clientHeight + 5;
157+
158+
if (isScrollable) {
159+
outputEl.classList.add('scrollable');
160+
161+
// Remove indicator after user interacts
162+
let hasInteracted = false;
163+
164+
const hideIndicator = () => {
165+
if (!hasInteracted) {
166+
hasInteracted = true;
167+
outputEl.classList.add('scrolled');
168+
}
169+
};
170+
171+
// Hide on scroll
172+
outputEl.addEventListener('scroll', hideIndicator, { once: true });
173+
174+
// Hide on touch
175+
outputEl.addEventListener('touchstart', hideIndicator, { once: true });
176+
177+
// Hide on mouse down (for desktop)
178+
outputEl.addEventListener('mousedown', hideIndicator, { once: true });
179+
} else {
180+
outputEl.classList.remove('scrollable', 'scrolled');
181+
}
182+
}
183+
151184
// Handle run button clicks
152185
document.querySelectorAll('.run-button').forEach((button) => {
153186
button.addEventListener('click', async function() {
@@ -168,7 +201,7 @@ const { code, title = "Python Code" } = Astro.props;
168201
btn.disabled = true;
169202
btn.textContent = 'Running...';
170203
output.textContent = 'Loading Python environment...';
171-
output.classList.remove('error');
204+
output.classList.remove('error', 'scrollable', 'scrolled');
172205

173206
try {
174207
// Load Pyodide if not already loaded
@@ -192,10 +225,16 @@ sys.stdout = StringIO()
192225

193226
output.textContent = outputText || '(No output)';
194227

228+
// Check if output is scrollable after content is set
229+
setTimeout(() => checkOutputScrollable(output), 100);
230+
195231
} catch (error) {
196232
output.textContent = `Error: ${error}`;
197233
output.classList.add('error');
198234
console.error('Python execution error:', error);
235+
236+
// Check scrollability even for errors
237+
setTimeout(() => checkOutputScrollable(output), 100);
199238
} finally {
200239
btn.disabled = false;
201240
btn.textContent = 'Run Code';
@@ -361,13 +400,104 @@ sys.stdout = StringIO()
361400
color: var(--text-primary);
362401
min-height: 3rem;
363402
background: var(--bg-secondary);
364-
white-space: pre-wrap;
403+
white-space: pre;
404+
overflow-x: auto;
405+
overflow-y: auto;
406+
max-height: 400px;
407+
/* Better scrollbar visibility */
408+
scrollbar-width: thin;
409+
scrollbar-color: var(--accent-primary) transparent;
410+
}
411+
412+
/* WebKit scrollbar styling for output */
413+
.output-content::-webkit-scrollbar {
414+
height: 8px;
415+
width: 8px;
416+
}
417+
418+
.output-content::-webkit-scrollbar-track {
419+
background: rgba(0, 0, 0, 0.1);
420+
border-radius: 4px;
421+
}
422+
423+
.output-content::-webkit-scrollbar-thumb {
424+
background: var(--accent-primary);
425+
border-radius: 4px;
426+
}
427+
428+
.output-content::-webkit-scrollbar-thumb:hover {
429+
background: var(--accent-secondary);
430+
}
431+
432+
/* Light theme scrollbar */
433+
:root[data-theme="light"] .output-content::-webkit-scrollbar-track {
434+
background: rgba(0, 0, 0, 0.05);
435+
}
436+
437+
:root[data-theme="light"] .output-content::-webkit-scrollbar-thumb {
438+
background: var(--accent-primary);
365439
}
366440

367441
.output-content.error {
368442
color: #e1695d;
369443
}
370444

445+
/* Swipe indicator for scrollable output */
446+
.output-content.scrollable::after {
447+
content: '👆 Swipe';
448+
position: absolute;
449+
right: 10px;
450+
top: 50%;
451+
transform: translateY(-50%);
452+
background: var(--accent-primary);
453+
color: #fff;
454+
padding: 0.3rem 0.6rem;
455+
border-radius: 20px;
456+
font-size: 0.75rem;
457+
font-family: var(--font-sans);
458+
pointer-events: none;
459+
opacity: 1;
460+
animation: swipeHint 2s ease-in-out infinite;
461+
white-space: nowrap;
462+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
463+
z-index: 10;
464+
}
465+
466+
.output-content.scrollable {
467+
position: relative;
468+
}
469+
470+
@keyframes swipeHint {
471+
0% {
472+
transform: translateY(-50%) translateX(0);
473+
}
474+
25% {
475+
transform: translateY(-50%) translateX(-10px);
476+
}
477+
50% {
478+
transform: translateY(-50%) translateX(0);
479+
}
480+
75% {
481+
transform: translateY(-50%) translateX(-10px);
482+
}
483+
100% {
484+
transform: translateY(-50%) translateX(0);
485+
}
486+
}
487+
488+
/* Hide swipe indicator after user interacts */
489+
.output-content.scrollable.scrolled::after {
490+
display: none;
491+
}
492+
493+
@media (max-width: 520px) {
494+
/* Larger scrollbar on mobile for better visibility */
495+
.output-content::-webkit-scrollbar {
496+
height: 10px;
497+
width: 10px;
498+
}
499+
}
500+
371501
/* Light theme support */
372502
:root[data-theme="light"] .pyodide-runner {
373503
background: #ffffff;

0 commit comments

Comments
 (0)