almost there for function

This commit is contained in:
Michael Freno
2025-12-18 01:18:38 -05:00
parent 1142d6f126
commit a19ad0cb36
5 changed files with 184 additions and 19 deletions

View File

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

View File

@@ -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 (
<div class="flex max-w-screen flex-row">
{/* Backdrop overlay - visible on mobile when sidebar is open */}
<div
class="fixed inset-0 bg-black transition-opacity duration-500 ease-out md:hidden z-40"
{/* Hamburger menu button - only visible on non-touch devices under mobile breakpoint */}
<button
onClick={() => 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
}}
>
<svg
class="w-6 h-6 text-text"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<LeftBar />
<div

View File

@@ -3,9 +3,11 @@ import { useBars } from "~/context/bars";
import { onMount, createEffect } from "solid-js";
export function LeftBar() {
const { setLeftBarSize, leftBarVisible } = useBars();
const { setLeftBarSize, leftBarVisible, setLeftBarVisible } = useBars();
let ref: HTMLDivElement | undefined;
let actualWidth = 0;
let touchStartX = 0;
let touchStartY = 0;
onMount(() => {
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 (
<nav
ref={ref}
@@ -113,7 +196,9 @@ export function RightBar() {
});
resizeObserver.observe(ref);
return () => resizeObserver.disconnect();
return () => {
resizeObserver.disconnect();
};
}
});

View File

@@ -1,5 +1,6 @@
import { Accessor, createContext, useContext } from "solid-js";
import { createSignal } from "solid-js";
import { hapticFeedback } from "~/lib/client-utils";
const BarsContext = createContext<{
leftBarSize: Accessor<number>;
@@ -38,8 +39,19 @@ export function BarsProvider(props: { children: any }) {
const [leftBarSize, setLeftBarSize] = createSignal(0);
const [rightBarSize, setRightBarSize] = createSignal(0);
const [centerWidth, setCenterWidth] = createSignal(0);
const [leftBarVisible, setLeftBarVisible] = createSignal(true);
const [rightBarVisible, setRightBarVisible] = createSignal(true);
const [leftBarVisible, _setLeftBarVisible] = createSignal(true);
const [rightBarVisible, _setRightBarVisible] = createSignal(true);
// Wrap visibility setters with haptic feedback
const setLeftBarVisible = (visible: boolean) => {
hapticFeedback(50);
_setLeftBarVisible(visible);
};
const setRightBarVisible = (visible: boolean) => {
hapticFeedback(50);
_setRightBarVisible(visible);
};
const toggleLeftBar = () => setLeftBarVisible(!leftBarVisible());
const toggleRightBar = () => setRightBarVisible(!rightBarVisible());

14
src/lib/client-utils.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Client-side utility functions
* Note: These utilities should only run in the browser
*/
/**
* Triggers haptic feedback on mobile devices
* @param duration - Duration in milliseconds (default 50ms for a light tap)
*/
export function hapticFeedback(duration: number = 50) {
if (typeof window !== "undefined" && "vibrate" in navigator) {
navigator.vibrate(duration);
}
}