@@ -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