feat(ui): Improve mobile navigation and scrolling in device settings

This commit is contained in:
Adam Shiervani 2025-02-27 13:17:37 +01:00
parent 4cbc2053e9
commit a4a6ded17f
5 changed files with 166 additions and 89 deletions

View File

@ -262,7 +262,7 @@ export default function Actionbar({
/> />
</div> </div>
<div className="hidden xs:block "> <div>
<Button <Button
size="XS" size="XS"
theme="light" theme="light"

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { forwardRef } from "react";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
type CardPropsType = { type CardPropsType = {
@ -24,9 +24,10 @@ export const GridCard = ({
); );
}; };
export default function Card({ children, className }: CardPropsType) { const Card = forwardRef<HTMLDivElement, CardPropsType>(({ children, className }, ref) => {
return ( return (
<div <div
ref={ref}
className={cx( className={cx(
"w-full rounded border-none bg-white shadow outline outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20", "w-full rounded border-none bg-white shadow outline outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20",
className, className,
@ -35,4 +36,8 @@ export default function Card({ children, className }: CardPropsType) {
{children} {children}
</div> </div>
); );
} });
Card.displayName = "Card";
export default Card;

View File

@ -105,7 +105,7 @@ video::-webkit-media-controls {
} }
.controlArrows { .controlArrows {
@apply flex items-center justify-between w-full md:w-1/5; @apply flex w-full items-center justify-between md:w-1/5;
flex-flow: column; flex-flow: column;
} }
@ -191,3 +191,13 @@ video::-webkit-media-controls {
scrollbar-color: theme("colors.gray.900") #002b36; scrollbar-color: theme("colors.gray.900") #002b36;
scrollbar-width: thin; scrollbar-width: thin;
} }
.hide-scrollbar {
overflow-y: scroll;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}

View File

@ -65,9 +65,9 @@ export default function SettingsKeyboardMouseRoute() {
</SettingsItem> </SettingsItem>
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem title="Modes" description="Choose the mouse input mode" /> <SettingsItem title="Modes" description="Choose the mouse input mode" />
<div className="flex items-center gap-4"> <div className="flex flex-col items-center gap-4 md:flex-row">
<button <button
className="group block grow" className="group block w-full grow"
onClick={() => console.log("Absolute mouse mode clicked")} onClick={() => console.log("Absolute mouse mode clicked")}
> >
<GridCard> <GridCard>
@ -95,7 +95,10 @@ export default function SettingsKeyboardMouseRoute() {
</div> </div>
</GridCard> </GridCard>
</button> </button>
<button className="group block grow cursor-not-allowed opacity-50" disabled> <button
className="group block w-full grow cursor-not-allowed opacity-50"
disabled
>
<GridCard> <GridCard>
<div className="group flex items-center gap-x-4 px-4 py-3"> <div className="group flex items-center gap-x-4 px-4 py-3">
<img <img
@ -114,6 +117,7 @@ export default function SettingsKeyboardMouseRoute() {
</div> </div>
<CheckCircleIcon <CheckCircleIcon
className={cx( className={cx(
"hidden",
"h-4 w-4 text-blue-700 transition-opacity duration-300 dark:text-blue-500", "h-4 w-4 text-blue-700 transition-opacity duration-300 dark:text-blue-500",
)} )}
/> />

View File

@ -11,16 +11,50 @@ import {
LuPalette, LuPalette,
} from "react-icons/lu"; } from "react-icons/lu";
import { LinkButton } from "../components/Button"; import { LinkButton } from "../components/Button";
import React, { useEffect } from "react"; import React, { useEffect, useRef, useState } from "react";
import { cx } from "../cva.config"; import { cx } from "../cva.config";
import { useUiStore } from "../hooks/stores"; import { useUiStore } from "../hooks/stores";
import useKeyboard from "../hooks/useKeyboard"; import useKeyboard from "../hooks/useKeyboard";
import { useResizeObserver } from "../hooks/useResizeObserver";
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
export default function SettingsRoute() { export default function SettingsRoute() {
const location = useLocation(); const location = useLocation();
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const { sendKeyboardEvent } = useKeyboard(); const { sendKeyboardEvent } = useKeyboard();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftGradient, setShowLeftGradient] = useState(false);
const [showRightGradient, setShowRightGradient] = useState(false);
const { width } = useResizeObserver({ ref: scrollContainerRef });
// Handle scroll position to show/hide gradients
const handleScroll = () => {
if (scrollContainerRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
// Show left gradient only if scrolled to the right
setShowLeftGradient(scrollLeft > 0);
// Show right gradient only if there's more content to scroll to the right
setShowRightGradient(scrollLeft < scrollWidth - clientWidth - 1); // -1 for rounding errors
}
};
useEffect(() => {
// Check initial scroll position
handleScroll();
// Add scroll event listener to the container
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
}
return () => {
// Clean up event listener
if (scrollContainer) {
scrollContainer.removeEventListener("scroll", handleScroll);
}
};
}, [width]);
useEffect(() => { useEffect(() => {
// disable focus trap // disable focus trap
@ -42,9 +76,9 @@ export default function SettingsRoute() {
return ( return (
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white"> <div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">
<div className="h-full"> <div className="h-full">
<div className="grid w-full gap-x-8 gap-y-4 md:grid-cols-8"> <div className="w-full gap-x-8 gap-y-4 space-y-4 md:grid md:grid-cols-8 md:space-y-0">
<div className="w-full select-none space-y-4 md:col-span-2"> <div className="w-full select-none space-y-4 md:col-span-2">
<Card className="flex w-full gap-x-4 p-2 md:flex-col dark:bg-slate-800"> <Card className="flex w-full gap-x-4 overflow-hidden p-2 md:flex-col dark:bg-slate-800">
<LinkButton <LinkButton
to=".." to=".."
size="SM" size="SM"
@ -55,84 +89,108 @@ export default function SettingsRoute() {
fullWidth fullWidth
/> />
</Card> </Card>
<Card className="flex w-full gap-x-4 p-2 md:flex-col dark:bg-slate-800"> <Card className="relative overflow-hidden">
<div> {/* Gradient overlay for left side - only visible on mobile when scrolled */}
<NavLink <div
to="general" className={cx(
className={({ isActive }) => (isActive ? "active" : "")} "pointer-events-none absolute inset-y-0 left-0 z-10 w-8 bg-gradient-to-r from-white to-transparent transition-opacity duration-300 ease-in-out md:hidden dark:from-slate-900",
> {
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> "opacity-0": !showLeftGradient,
<LuSettings className="h-4 w-4 shrink-0" /> "opacity-100": showLeftGradient,
<h1>General</h1> },
</div> )}
</NavLink> ></div>
</div> {/* Gradient overlay for right side - only visible on mobile when there's more content */}
<div
<div> className={cx(
<NavLink "pointer-events-none absolute inset-y-0 right-0 z-10 w-8 bg-gradient-to-l from-white to-transparent transition duration-300 ease-in-out md:hidden dark:from-slate-900",
to="mouse" {
className={({ isActive }) => (isActive ? "active" : "")} "opacity-0": !showRightGradient,
> "opacity-100": showRightGradient,
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> },
<LuKeyboard className="h-4 w-4 shrink-0" /> )}
<h1>Mouse</h1> ></div>
</div> <div
</NavLink> ref={scrollContainerRef}
</div> className="hide-scrollbar relative flex w-full gap-x-4 overflow-x-auto whitespace-nowrap p-2 md:flex-col md:overflow-visible md:whitespace-normal dark:bg-slate-800"
<div> >
<NavLink <div className="shrink-0">
to="video" <NavLink
className={({ isActive }) => (isActive ? "active" : "")} to="general"
> className={({ isActive }) => (isActive ? "active" : "")}
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> >
<LuVideo className="h-4 w-4 shrink-0" /> <div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<h1>Video</h1> <LuSettings className="h-4 w-4 shrink-0" />
</div> <h1>General</h1>
</NavLink> </div>
</div> </NavLink>
<div> </div>
<NavLink <div className="shrink-0">
to="hardware" <NavLink
className={({ isActive }) => (isActive ? "active" : "")} to="mouse"
> className={({ isActive }) => (isActive ? "active" : "")}
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> >
<LuCpu className="h-4 w-4 shrink-0" /> <div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<h1>Hardware</h1> <LuKeyboard className="h-4 w-4 shrink-0" />
</div> <h1>Mouse</h1>
</NavLink> </div>
</div> </NavLink>
<div> </div>
<NavLink <div className="shrink-0">
to="security" <NavLink
className={({ isActive }) => (isActive ? "active" : "")} to="video"
> className={({ isActive }) => (isActive ? "active" : "")}
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> >
<LuShieldCheck className="h-4 w-4 shrink-0" /> <div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<h1>Security</h1> <LuVideo className="h-4 w-4 shrink-0" />
</div> <h1>Video</h1>
</NavLink> </div>
</div> </NavLink>
<div> </div>
<NavLink <div className="shrink-0">
to="appearance" <NavLink
className={({ isActive }) => (isActive ? "active" : "")} to="hardware"
> className={({ isActive }) => (isActive ? "active" : "")}
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> >
<LuPalette className="h-4 w-4 shrink-0" /> <div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<h1>Appearance</h1> <LuCpu className="h-4 w-4 shrink-0" />
</div> <h1>Hardware</h1>
</NavLink> </div>
</div> </NavLink>
<div> </div>
<NavLink <div className="shrink-0">
to="advanced" <NavLink
className={({ isActive }) => (isActive ? "active" : "")} to="security"
> className={({ isActive }) => (isActive ? "active" : "")}
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> >
<LuWrench className="h-4 w-4 shrink-0" /> <div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<h1>Advanced</h1> <LuShieldCheck className="h-4 w-4 shrink-0" />
</div> <h1>Security</h1>
</NavLink> </div>
</NavLink>
</div>
<div className="shrink-0">
<NavLink
to="appearance"
className={({ isActive }) => (isActive ? "active" : "")}
>
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<LuPalette className="h-4 w-4 shrink-0" />
<h1>Appearance</h1>
</div>
</NavLink>
</div>
<div className="shrink-0">
<NavLink
to="advanced"
className={({ isActive }) => (isActive ? "active" : "")}
>
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<LuWrench className="h-4 w-4 shrink-0" />
<h1>Advanced</h1>
</div>
</NavLink>
</div>
</div> </div>
</Card> </Card>
</div> </div>