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>
|
||||||
|
|
||||||
<div className="hidden xs:block ">
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,8 +89,32 @@ 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 */}
|
||||||
|
<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
|
<NavLink
|
||||||
to="general"
|
to="general"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
@ -67,8 +125,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="shrink-0">
|
||||||
<div>
|
|
||||||
<NavLink
|
<NavLink
|
||||||
to="mouse"
|
to="mouse"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
@ -79,7 +136,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="shrink-0">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="video"
|
to="video"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
@ -90,7 +147,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="shrink-0">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="hardware"
|
to="hardware"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
@ -101,7 +158,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="shrink-0">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="security"
|
to="security"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
@ -112,7 +169,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="shrink-0">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="appearance"
|
to="appearance"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
@ -123,7 +180,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="shrink-0">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="advanced"
|
to="advanced"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
@ -134,6 +191,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:col-span-5">
|
<div className="w-full md:col-span-5">
|
||||||
|
|
Loading…
Reference in New Issue