mirror of https://github.com/jetkvm/kvm.git
feat(ui): Improve mobile navigation and scrolling in device settings
This commit is contained in:
parent
4cbc2053e9
commit
a4a6ded17f
|
@ -262,7 +262,7 @@ export default function Actionbar({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="hidden xs:block ">
|
||||
<div>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
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 (
|
||||
<div
|
||||
ref={ref}
|
||||
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",
|
||||
className,
|
||||
|
@ -35,4 +36,8 @@ export default function Card({ children, className }: CardPropsType) {
|
|||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Card.displayName = "Card";
|
||||
|
||||
export default Card;
|
||||
|
|
|
@ -105,7 +105,7 @@ video::-webkit-media-controls {
|
|||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
@ -191,3 +191,13 @@ video::-webkit-media-controls {
|
|||
scrollbar-color: theme("colors.gray.900") #002b36;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -65,9 +65,9 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
</SettingsItem>
|
||||
<div className="space-y-4">
|
||||
<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
|
||||
className="group block grow"
|
||||
className="group block w-full grow"
|
||||
onClick={() => console.log("Absolute mouse mode clicked")}
|
||||
>
|
||||
<GridCard>
|
||||
|
@ -95,7 +95,10 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
</div>
|
||||
</GridCard>
|
||||
</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>
|
||||
<div className="group flex items-center gap-x-4 px-4 py-3">
|
||||
<img
|
||||
|
@ -114,6 +117,7 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
</div>
|
||||
<CheckCircleIcon
|
||||
className={cx(
|
||||
"hidden",
|
||||
"h-4 w-4 text-blue-700 transition-opacity duration-300 dark:text-blue-500",
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -11,16 +11,50 @@ import {
|
|||
LuPalette,
|
||||
} from "react-icons/lu";
|
||||
import { LinkButton } from "../components/Button";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { cx } from "../cva.config";
|
||||
import { useUiStore } from "../hooks/stores";
|
||||
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. */
|
||||
export default function SettingsRoute() {
|
||||
const location = useLocation();
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
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(() => {
|
||||
// disable focus trap
|
||||
|
@ -42,9 +76,9 @@ export default function SettingsRoute() {
|
|||
return (
|
||||
<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="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">
|
||||
<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
|
||||
to=".."
|
||||
size="SM"
|
||||
|
@ -55,84 +89,108 @@ export default function SettingsRoute() {
|
|||
fullWidth
|
||||
/>
|
||||
</Card>
|
||||
<Card className="flex w-full gap-x-4 p-2 md:flex-col dark:bg-slate-800">
|
||||
<div>
|
||||
<NavLink
|
||||
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">
|
||||
<LuSettings className="h-4 w-4 shrink-0" />
|
||||
<h1>General</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NavLink
|
||||
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">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<h1>Mouse</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div>
|
||||
<NavLink
|
||||
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">
|
||||
<LuVideo className="h-4 w-4 shrink-0" />
|
||||
<h1>Video</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div>
|
||||
<NavLink
|
||||
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">
|
||||
<LuCpu className="h-4 w-4 shrink-0" />
|
||||
<h1>Hardware</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div>
|
||||
<NavLink
|
||||
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">
|
||||
<LuShieldCheck className="h-4 w-4 shrink-0" />
|
||||
<h1>Security</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<Card className="relative overflow-hidden">
|
||||
{/* Gradient overlay for left side - only visible on mobile when scrolled */}
|
||||
<div
|
||||
className={cx(
|
||||
"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",
|
||||
{
|
||||
"opacity-0": !showLeftGradient,
|
||||
"opacity-100": showLeftGradient,
|
||||
},
|
||||
)}
|
||||
></div>
|
||||
{/* Gradient overlay for right side - only visible on mobile when there's more content */}
|
||||
<div
|
||||
className={cx(
|
||||
"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",
|
||||
{
|
||||
"opacity-0": !showRightGradient,
|
||||
"opacity-100": showRightGradient,
|
||||
},
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
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 className="shrink-0">
|
||||
<NavLink
|
||||
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">
|
||||
<LuSettings className="h-4 w-4 shrink-0" />
|
||||
<h1>General</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
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">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<h1>Mouse</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
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">
|
||||
<LuVideo className="h-4 w-4 shrink-0" />
|
||||
<h1>Video</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
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">
|
||||
<LuCpu className="h-4 w-4 shrink-0" />
|
||||
<h1>Hardware</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
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">
|
||||
<LuShieldCheck className="h-4 w-4 shrink-0" />
|
||||
<h1>Security</h1>
|
||||
</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>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue