Files
kvm/ui/src/components/Header.tsx
Marc Brooks 7ccb8e617c chore: Upgrade UI vite and tailwind packages (#443)
* chore: Upgrade UI vite and tailwind packages

Vite 5.2.0 -> 6.3.5
@vitejs/plugin-basic-ssl 1.2.0 -> 2.0.0
cva: 1.0.0-beta.1 -> 1.0.0-beta.3
focus-trap-react 10.2.3 -> 11.0.3
framer-motion 11.15.0 -> 12.11.0
@tailwindcss/postcss 4.1.6
@tailwindcss/vite 4.1.6
tailwind 3.4.17 -> 4.1.6
tailwind-merge 2.5.5 -> 3.3.0

Minor updates:
@headlessui/react 2.2.2 -> 2.2.3
@types/react 19.1.3 -> 19.1.4
@types/react-dom 19.1.3 -> 19.1.5
@typescript-eslint/eslint-plugin 8.32.0 -> 8.32.1
@typescript-eslint/parser 8.32.0 -> 8.32.1
react-simple-keyboard 3.8.71 -> 3.8.72

The new version of vite required an Node 22.15 (since that's current LTS and node 21.x is EOL)

The changes to css due to the tailwind 3 to 4 upgrade were done following [the upgrade guide](https://tailwindcss.com/docs/upgrade-guide#changes-from-v3)

Done in this order (important):
`shadow-sm` -> `shadow-xs`
`shadow` -> `shadown-sm`
`rounded` -> `rounded-sm`
`outline-none` -> `outline-hidden`
`32rem_32rem_at_center` -> `center_at_32rem_32rem` (revised order of gradient props)
`ring-1 ring-black ring-opacity-5` -> `ring-1 ring-black/50`
`flex-shrink-0` -> `shrink-0`
`flex-grow-0` -> `grow-0`
`outline outline-1` -> `outline-1`

ALSO removed the **extra** `opacity-0` on the video element (trips up latest tailwind causing the video to be invisible)

FocusTrap is now not exported as the default, so change those imports

headlessui's Menu completely changed, so upgrade to the new syntax which necessitated a reorganization of the Header.tsx to enable the "menu" to still work

* Update eslint config and fix errors
2025-05-15 14:21:03 +02:00

164 lines
6.8 KiB
TypeScript

import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { LuMonitorSmartphone } from "react-icons/lu";
import Container from "@/components/Container";
import Card from "@/components/Card";
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
import LogoBlueIcon from "@/assets/logo-blue.svg";
import LogoWhiteIcon from "@/assets/logo-white.svg";
import USBStateStatus from "@components/USBStateStatus";
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
import { CLOUD_API, DEVICE_API } from "@/ui.config";
import api from "../api";
import { isOnDevice } from "../main";
import { LinkButton } from "./Button";
interface NavbarProps {
isLoggedIn: boolean;
primaryLinks?: { title: string; to: string }[];
userEmail?: string;
showConnectionStatus?: boolean;
picture?: string;
kvmName?: string;
}
export default function DashboardNavbar({
primaryLinks = [],
isLoggedIn,
showConnectionStatus,
userEmail,
picture,
kvmName,
}: NavbarProps) {
const peerConnectionState = useRTCStore(state => state.peerConnectionState);
const setUser = useUserStore(state => state.setUser);
const navigate = useNavigate();
const onLogout = useCallback(async () => {
const logoutUrl = isOnDevice ? `${DEVICE_API}/auth/logout` : `${CLOUD_API}/logout`;
const res = await api.POST(logoutUrl);
if (!res.ok) return;
setUser(null);
// The root route will redirect to appropriate login page, be it the local one or the cloud one
navigate("/");
}, [navigate, setUser]);
const usbState = useHidStore(state => state.usbState);
// for testing
//userEmail = "user@example.org";
//picture = "https://placehold.co/32x32"
return (
<div className="w-full select-none border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
<Container>
<div className="flex h-14 items-center justify-between">
<div className="flex shrink-0 items-center gap-x-8">
<div className="inline-block shrink-0">
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
<img src={LogoWhiteIcon} alt="" className="hidden h-[24px] dark:block" />
</div>
<div className="flex gap-x-2">
{primaryLinks.map(({ title, to }, i) => {
return (
<LinkButton
key={i + title}
theme="blank"
size="SM"
text={title}
to={to}
LeadingIcon={LuMonitorSmartphone}
/>
);
})}
</div>
</div>
<div className="flex w-full items-center justify-end gap-x-2">
<div className="flex shrink-0 items-center space-x-4">
<div className="hidden items-center gap-x-2 md:flex">
{showConnectionStatus && (
<>
<div className="w-[159px]">
<PeerConnectionStatusCard
state={peerConnectionState}
title={kvmName}
/>
</div>
<div className="hidden w-[159px] md:block">
<USBStateStatus
state={usbState}
peerConnectionState={peerConnectionState}
/>
</div>
</>
)}
{isLoggedIn ? (
<>
<hr className="h-[20px] w-[1px] border-none bg-slate-800/20 dark:bg-slate-300/20" />
<div className="relative inline-block text-left">
<Menu>
<MenuButton>
<Button className="flex items-center gap-x-3 rounded-md border bg-white dark:border-slate-600 dark:bg-slate-800 dark:text-white border-slate-800/20 px-2 py-1.5">
{picture
? (
<img
src={picture}
alt="Avatar"
className="size-6 rounded-full border-2 border-transparent transition-colors group-hover:border-blue-700"
/>
)
: (
<span className="max-w-[200px] text-sm/6 font-display font-semibold truncate">
{userEmail}
</span>
)
}
<ChevronDownIcon className="size-4 shrink-0 text-slate-900 dark:text-white" />
</Button>
</MenuButton>
<MenuItems
transition
anchor="bottom end"
className="right-0 mt-1 w-56 origin-top-right data-closed:opacity-0 focus:outline-hidden">
<MenuItem>
<Card className="overflow-hidden">
<div className="space-y-1 p-1 dark:text-white">
{userEmail && (
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
<div className="p-2">
<div className="font-display text-xs">Logged in as</div>
<div className="max-w-[200px] truncate font-display text-sm font-semibold">
{userEmail}
</div>
</div>
</div>
)}
</div>
<div className="space-y-1 p-1 dark:text-white" onClick={onLogout}>
<button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
<ArrowLeftEndOnRectangleIcon className="size-4" />
<div className="font-display">Log out</div>
</button>
</div>
</Card>
</MenuItem>
</MenuItems>
</Menu>
</div>
</>
) : null}
</div>
</div>
</div>
</div>
</Container>
</div>
);
}