almost there for function
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
71
src/app.tsx
71
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";
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
14
src/lib/client-utils.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user