almost there for function
This commit is contained in:
@@ -465,3 +465,10 @@ input[type="checkbox"]:checked::before {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
75
src/app.tsx
75
src/app.tsx
@@ -1,6 +1,12 @@
|
|||||||
import { Router } from "@solidjs/router";
|
import { Router } from "@solidjs/router";
|
||||||
import { FileRoutes } from "@solidjs/start/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 "./app.css";
|
||||||
import { LeftBar, RightBar } from "./components/Bars";
|
import { LeftBar, RightBar } from "./components/Bars";
|
||||||
import { TerminalSplash } from "./components/TerminalSplash";
|
import { TerminalSplash } from "./components/TerminalSplash";
|
||||||
@@ -29,13 +35,13 @@ function AppLayout(props: { children: any }) {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const isMobile = window.innerWidth < 768; // md breakpoint
|
const isMobile = window.innerWidth < 768; // md breakpoint
|
||||||
|
|
||||||
// Show bars when switching to desktop
|
// Show bars when switching to desktop
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
setLeftBarVisible(true);
|
setLeftBarVisible(true);
|
||||||
setRightBarVisible(true);
|
setRightBarVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
|
const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
|
||||||
setCenterWidth(newWidth);
|
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
|
// Swipe gestures to reveal bars
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let touchStartX = 0;
|
let touchStartX = 0;
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
const EDGE_THRESHOLD = 50; // pixels from edge to trigger
|
const EDGE_THRESHOLD = 100;
|
||||||
const SWIPE_THRESHOLD = 100; // minimum swipe distance
|
const SWIPE_THRESHOLD = 100;
|
||||||
|
|
||||||
const handleTouchStart = (e: TouchEvent) => {
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
touchStartX = e.touches[0].clientX;
|
touchStartX = e.touches[0].clientX;
|
||||||
@@ -111,7 +139,10 @@ function AppLayout(props: { children: any }) {
|
|||||||
setLeftBarVisible(true);
|
setLeftBarVisible(true);
|
||||||
}
|
}
|
||||||
// Swipe left from right edge - reveal right bar
|
// 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);
|
setRightBarVisible(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,16 +160,32 @@ function AppLayout(props: { children: any }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex max-w-screen flex-row">
|
<div class="flex max-w-screen flex-row">
|
||||||
{/* Backdrop overlay - visible on mobile when sidebar is open */}
|
{/* Hamburger menu button - only visible on non-touch devices under mobile breakpoint */}
|
||||||
<div
|
<button
|
||||||
class="fixed inset-0 bg-black transition-opacity duration-500 ease-out md:hidden z-40"
|
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={{
|
classList={{
|
||||||
"opacity-50 pointer-events-auto": leftBarVisible(),
|
"hidden": leftBarVisible()
|
||||||
"opacity-0 pointer-events-none": !leftBarVisible()
|
|
||||||
}}
|
}}
|
||||||
onClick={() => setLeftBarVisible(false)}
|
aria-label="Toggle navigation menu"
|
||||||
aria-label="Close sidebar"
|
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 />
|
<LeftBar />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { useBars } from "~/context/bars";
|
|||||||
import { onMount, createEffect } from "solid-js";
|
import { onMount, createEffect } from "solid-js";
|
||||||
|
|
||||||
export function LeftBar() {
|
export function LeftBar() {
|
||||||
const { setLeftBarSize, leftBarVisible } = useBars();
|
const { setLeftBarSize, leftBarVisible, setLeftBarVisible } = useBars();
|
||||||
let ref: HTMLDivElement | undefined;
|
let ref: HTMLDivElement | undefined;
|
||||||
let actualWidth = 0;
|
let actualWidth = 0;
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
@@ -25,7 +27,72 @@ export function LeftBar() {
|
|||||||
});
|
});
|
||||||
resizeObserver.observe(ref);
|
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);
|
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 (
|
return (
|
||||||
<nav
|
<nav
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -113,7 +196,9 @@ export function RightBar() {
|
|||||||
});
|
});
|
||||||
resizeObserver.observe(ref);
|
resizeObserver.observe(ref);
|
||||||
|
|
||||||
return () => resizeObserver.disconnect();
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Accessor, createContext, useContext } from "solid-js";
|
import { Accessor, createContext, useContext } from "solid-js";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
|
import { hapticFeedback } from "~/lib/client-utils";
|
||||||
|
|
||||||
const BarsContext = createContext<{
|
const BarsContext = createContext<{
|
||||||
leftBarSize: Accessor<number>;
|
leftBarSize: Accessor<number>;
|
||||||
@@ -38,8 +39,19 @@ export function BarsProvider(props: { children: any }) {
|
|||||||
const [leftBarSize, setLeftBarSize] = createSignal(0);
|
const [leftBarSize, setLeftBarSize] = createSignal(0);
|
||||||
const [rightBarSize, setRightBarSize] = createSignal(0);
|
const [rightBarSize, setRightBarSize] = createSignal(0);
|
||||||
const [centerWidth, setCenterWidth] = createSignal(0);
|
const [centerWidth, setCenterWidth] = createSignal(0);
|
||||||
const [leftBarVisible, setLeftBarVisible] = createSignal(true);
|
const [leftBarVisible, _setLeftBarVisible] = createSignal(true);
|
||||||
const [rightBarVisible, setRightBarVisible] = 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 toggleLeftBar = () => setLeftBarVisible(!leftBarVisible());
|
||||||
const toggleRightBar = () => setRightBarVisible(!rightBarVisible());
|
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