From a19ad0cb36ec5d186019de58f9c78445da4a8092 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 18 Dec 2025 01:18:38 -0500 Subject: [PATCH] almost there for function --- src/app.css | 7 ++++ src/app.tsx | 75 ++++++++++++++++++++++++++------- src/components/Bars.tsx | 91 +++++++++++++++++++++++++++++++++++++++-- src/context/bars.tsx | 16 +++++++- src/lib/client-utils.ts | 14 +++++++ 5 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 src/lib/client-utils.ts diff --git a/src/app.css b/src/app.css index 5881e52..584da6a 100644 --- a/src/app.css +++ b/src/app.css @@ -465,3 +465,10 @@ input[type="checkbox"]:checked::before { width: 100%; height: 100%; } + +/* Hamburger menu button - only show on non-touch devices under mobile breakpoint */ +@media (max-width: 767px) and (hover: hover) and (pointer: fine) { + .hamburger-menu-btn { + display: block !important; + } +} diff --git a/src/app.tsx b/src/app.tsx index 461bb9a..30b2747 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,6 +1,12 @@ import { Router } from "@solidjs/router"; import { FileRoutes } from "@solidjs/start/router"; -import { createEffect, ErrorBoundary, Suspense, onMount, onCleanup } from "solid-js"; +import { + createEffect, + ErrorBoundary, + Suspense, + onMount, + onCleanup +} from "solid-js"; import "./app.css"; import { LeftBar, RightBar } from "./components/Bars"; import { TerminalSplash } from "./components/TerminalSplash"; @@ -29,13 +35,13 @@ function AppLayout(props: { children: any }) { createEffect(() => { const handleResize = () => { const isMobile = window.innerWidth < 768; // md breakpoint - + // Show bars when switching to desktop if (!isMobile) { setLeftBarVisible(true); setRightBarVisible(true); } - + const newWidth = window.innerWidth - leftBarSize() - rightBarSize(); setCenterWidth(newWidth); }; @@ -77,12 +83,34 @@ function AppLayout(props: { children: any }) { }); }); + // ESC key to close sidebars on mobile + onMount(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const isMobile = window.innerWidth < 768; // md breakpoint + + if (e.key === "Escape" && isMobile) { + if (leftBarVisible()) { + setLeftBarVisible(false); + } + if (rightBarVisible()) { + setRightBarVisible(false); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + + onCleanup(() => { + window.removeEventListener("keydown", handleKeyDown); + }); + }); + // Swipe gestures to reveal bars onMount(() => { let touchStartX = 0; let touchStartY = 0; - const EDGE_THRESHOLD = 50; // pixels from edge to trigger - const SWIPE_THRESHOLD = 100; // minimum swipe distance + const EDGE_THRESHOLD = 100; + const SWIPE_THRESHOLD = 100; const handleTouchStart = (e: TouchEvent) => { touchStartX = e.touches[0].clientX; @@ -111,7 +139,10 @@ function AppLayout(props: { children: any }) { setLeftBarVisible(true); } // Swipe left from right edge - reveal right bar - else if (touchStartX > window.innerWidth - EDGE_THRESHOLD && deltaX < -SWIPE_THRESHOLD) { + else if ( + touchStartX > window.innerWidth - EDGE_THRESHOLD && + deltaX < -SWIPE_THRESHOLD + ) { setRightBarVisible(true); } } @@ -129,16 +160,32 @@ function AppLayout(props: { children: any }) { return (
- {/* Backdrop overlay - visible on mobile when sidebar is open */} -
setLeftBarVisible(!leftBarVisible())} + class="hamburger-menu-btn fixed top-4 left-4 z-50 p-2 rounded-md bg-surface0 hover:bg-surface1 transition-colors shadow-md" classList={{ - "opacity-50 pointer-events-auto": leftBarVisible(), - "opacity-0 pointer-events-none": !leftBarVisible() + "hidden": leftBarVisible() }} - onClick={() => setLeftBarVisible(false)} - aria-label="Close sidebar" - /> + aria-label="Toggle navigation menu" + style={{ + display: "none" // Hidden by default, shown via media query for non-touch devices + }} + > + + + +
{ if (ref) { @@ -25,7 +27,72 @@ export function LeftBar() { }); resizeObserver.observe(ref); - return () => resizeObserver.disconnect(); + // Swipe-to-dismiss gesture on sidebar itself (mobile only) + const handleTouchStart = (e: TouchEvent) => { + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + }; + + const handleTouchEnd = (e: TouchEvent) => { + const isMobile = window.innerWidth < 768; + if (!isMobile) return; // Only allow dismiss on mobile + + const touchEndX = e.changedTouches[0].clientX; + const touchEndY = e.changedTouches[0].clientY; + const deltaX = touchEndX - touchStartX; + const deltaY = touchEndY - touchStartY; + + // Only trigger if horizontal swipe is dominant + if (Math.abs(deltaX) > Math.abs(deltaY)) { + // Swipe left to dismiss (at least 50px) + if (deltaX < -50 && leftBarVisible()) { + setLeftBarVisible(false); + } + } + }; + + // Focus trap for accessibility on mobile + const handleKeyDown = (e: KeyboardEvent) => { + const isMobile = window.innerWidth < 768; + + if (!isMobile || !leftBarVisible()) return; + + if (e.key === 'Tab') { + const focusableElements = ref?.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + ); + + if (!focusableElements || focusableElements.length === 0) return; + + const firstElement = focusableElements[0] as HTMLElement; + const lastElement = focusableElements[focusableElements.length - 1] 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(); + } + } + } + }; + + ref.addEventListener('touchstart', handleTouchStart, { passive: true }); + ref.addEventListener('touchend', handleTouchEnd, { passive: true }); + ref.addEventListener('keydown', handleKeyDown); + + return () => { + resizeObserver.disconnect(); + ref?.removeEventListener('touchstart', handleTouchStart); + ref?.removeEventListener('touchend', handleTouchEnd); + ref?.removeEventListener('keydown', handleKeyDown); + }; } }); @@ -34,6 +101,22 @@ export function LeftBar() { setLeftBarSize(leftBarVisible() ? actualWidth : 0); }); + // Auto-focus first element when sidebar opens on mobile + createEffect(() => { + const isMobile = window.innerWidth < 768; + + if (leftBarVisible() && isMobile && ref) { + const firstFocusable = ref.querySelector( + 'a[href], button:not([disabled]), input:not([disabled])' + ) as HTMLElement; + + if (firstFocusable) { + // Small delay to ensure animation has started + setTimeout(() => firstFocusable.focus(), 100); + } + } + }); + return (