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
This commit is contained in:
Marc Brooks
2025-05-15 07:21:03 -05:00
committed by GitHub
parent 340babac24
commit 7ccb8e617c
44 changed files with 2006 additions and 1381 deletions

View File

@@ -77,6 +77,7 @@ export default function DevicesIdDeregister() {
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
userEmail={user?.email}
picture={user?.picture}
kvmName={device?.name}
/>
<div className="w-full h-full">

View File

@@ -320,7 +320,7 @@ function ModeSelectionView({
].map(({ label, description, value: mode, icon: Icon, tag, disabled }, index) => (
<div
key={label}
className={cx("animate-fadeIn opacity-0")}
className={cx("animate-fadeIn")}
style={{
animationDuration: "0.7s",
animationDelay: `${25 * (index * 5)}ms`,
@@ -328,7 +328,7 @@ function ModeSelectionView({
>
<Card
className={cx(
"w-full min-w-[250px] cursor-pointer bg-white shadow-sm transition-all duration-100 hover:shadow-md dark:bg-slate-800",
"w-full min-w-[250px] cursor-pointer bg-white shadow-xs transition-all duration-100 hover:shadow-md dark:bg-slate-800",
{
"ring-2 ring-blue-700": selectedMode === mode,
"hover:ring-2 hover:ring-blue-500": selectedMode !== mode && !disabled,
@@ -373,7 +373,7 @@ function ModeSelectionView({
))}
</div>
<div
className="flex animate-fadeIn justify-end opacity-0"
className="flex animate-fadeIn justify-end"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",
@@ -437,7 +437,7 @@ function BrowserFileView({
className="block cursor-pointer select-none"
>
<div
className="group animate-fadeIn opacity-0"
className="group animate-fadeIn"
style={{
animationDuration: "0.7s",
}}
@@ -483,7 +483,7 @@ function BrowserFileView({
</div>
<div
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
className="flex w-full animate-fadeIn items-end justify-between"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@@ -578,7 +578,7 @@ function UrlView({
/>
<div
className="animate-fadeIn opacity-0"
className="animate-fadeIn"
style={{
animationDuration: "0.7s",
}}
@@ -593,7 +593,7 @@ function UrlView({
/>
</div>
<div
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
className="flex w-full animate-fadeIn items-end justify-between"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@@ -619,7 +619,7 @@ function UrlView({
<hr className="border-slate-800/30 dark:border-slate-300/20" />
<div
className="animate-fadeIn opacity-0"
className="animate-fadeIn"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",
@@ -797,7 +797,7 @@ function DeviceFileView({
description="Select an image to mount from the JetKVM storage"
/>
<div
className="w-full animate-fadeIn opacity-0"
className="w-full animate-fadeIn"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@@ -886,7 +886,7 @@ function DeviceFileView({
{onStorageFiles.length > 0 ? (
<div
className="flex animate-fadeIn items-end justify-between opacity-0"
className="flex animate-fadeIn items-end justify-between"
style={{
animationDuration: "0.7s",
animationDelay: "0.15s",
@@ -914,7 +914,7 @@ function DeviceFileView({
</div>
) : (
<div
className="flex animate-fadeIn items-end justify-end opacity-0"
className="flex animate-fadeIn items-end justify-end"
style={{
animationDuration: "0.7s",
animationDelay: "0.15s",
@@ -927,7 +927,7 @@ function DeviceFileView({
)}
<hr className="border-slate-800/20 dark:border-slate-300/20" />
<div
className="animate-fadeIn space-y-2 opacity-0"
className="animate-fadeIn space-y-2"
style={{
animationDuration: "0.7s",
animationDelay: "0.20s",
@@ -941,9 +941,9 @@ function DeviceFileView({
{percentageUsed}% used
</span>
</div>
<div className="h-3.5 w-full overflow-hidden rounded-sm bg-slate-200 dark:bg-slate-700">
<div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700">
<div
className="h-full rounded-sm bg-blue-700 transition-all duration-300 ease-in-out dark:bg-blue-500"
className="h-full rounded-xs bg-blue-700 transition-all duration-300 ease-in-out dark:bg-blue-500"
style={{ width: `${percentageUsed}%` }}
></div>
</div>
@@ -959,7 +959,7 @@ function DeviceFileView({
{onStorageFiles.length > 0 && (
<div
className="w-full animate-fadeIn opacity-0"
className="w-full animate-fadeIn"
style={{
animationDuration: "0.7s",
animationDelay: "0.25s",
@@ -1251,7 +1251,7 @@ function UploadFileView({
}
/>
<div
className="animate-fadeIn space-y-2 opacity-0"
className="animate-fadeIn space-y-2"
style={{
animationDuration: "0.7s",
}}
@@ -1365,7 +1365,7 @@ function UploadFileView({
{/* Display upload error if present */}
{uploadError && (
<div
className="mt-2 animate-fadeIn truncate text-sm text-red-600 opacity-0 dark:text-red-400"
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400"
style={{ animationDuration: "0.7s" }}
>
Error: {uploadError}
@@ -1373,7 +1373,7 @@ function UploadFileView({
)}
<div
className="flex w-full animate-fadeIn items-end opacity-0"
className="flex w-full animate-fadeIn items-end"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@@ -1496,7 +1496,7 @@ function PreUploadedImageItem({
</div>
<div className="relative flex select-none items-center gap-x-3">
<div
className={cx("opacity-0 transition-opacity duration-200", {
className={cx("opacity-0 transition-opacity duration-200", {
"w-auto opacity-100": isHovering,
})}
>

View File

@@ -81,6 +81,7 @@ export default function DeviceIdRename() {
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
userEmail={user?.email}
picture={user?.picture}
kvmName={device?.name}
/>
<div className="h-full w-full">

View File

@@ -26,7 +26,7 @@ export interface TLSState {
privateKey?: string;
}
export const loader = async () => {
const loader = async () => {
if (isOnDevice) {
const status = await api
.GET(`${DEVICE_API}/device`)
@@ -468,3 +468,5 @@ export default function SettingsAccessIndexRoute() {
</div>
);
}
SettingsAccessIndexRoute.loader = loader;

View File

@@ -175,8 +175,8 @@ export default function SettingsMacrosRoute() {
return (
<span key={stepIndex} className="inline-flex items-center">
<StepIcon className="mr-1 h-3 w-3 flex-shrink-0 text-slate-400 dark:text-slate-500" />
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
<StepIcon className="mr-1 h-3 w-3 shrink-0 text-slate-400 dark:text-slate-500" />
<span className="px-2 py-0.5 rounded-md border border-slate-200/50 dark:border-slate-700/50 bg-slate-50 dark:bg-slate-800">
{(Array.isArray(step.modifiers) &&
step.modifiers.length > 0) ||
(Array.isArray(step.keys) && step.keys.length > 0) ? (

View File

@@ -14,15 +14,14 @@ import {
useNetworkStateStore,
} from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
import InputField from "@components/InputField";
import { SettingsPageHeader } from "../components/SettingsPageheader";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
import Fieldset from "../components/Fieldset";
import { ConfirmDialog } from "../components/ConfirmDialog";
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import Fieldset from "@/components/Fieldset";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import notifications from "@/notifications";
import { SettingsItem } from "./devices.$id.settings";
@@ -51,9 +50,13 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
return () => clearInterval(interval);
}, [lifetime]);
if (lifetime == "") {
return <strong>N/A</strong>;
}
return (
<>
<span>{dayjs(lifetime).format("YYYY-MM-DD HH:mm")}</span>
<strong>{dayjs(lifetime).format("YYYY-MM-DD HH:mm")}</strong>
{remaining && (
<>
{" "}

View File

@@ -12,15 +12,16 @@ import {
LuNetwork,
} from "react-icons/lu";
import React, { useEffect, useRef, useState } from "react";
import { useResizeObserver } from "usehooks-ts";
import Card from "@/components/Card";
import { LinkButton } from "@/components/Button";
import LoadingSpinner from "@/components/LoadingSpinner";
import { useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import { LinkButton } from "../components/Button";
import { cx } from "../cva.config";
import { useUiStore } from "../hooks/stores";
import useKeyboard from "../hooks/useKeyboard";
import { useResizeObserver } from "usehooks-ts";
import LoadingSpinner from "../components/LoadingSpinner";
/* 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() {

View File

@@ -12,7 +12,7 @@ import {
useSearchParams,
} from "react-router-dom";
import { useInterval } from "usehooks-ts";
import FocusTrap from "focus-trap-react";
import { FocusTrap } from "focus-trap-react";
import { motion, AnimatePresence } from "framer-motion";
import useWebSocket from "react-use-websocket";
@@ -809,7 +809,7 @@ export default function KvmIdRoute() {
<WebRTCVideo />
<div
style={{ animationDuration: "500ms" }}
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4 opacity-0"
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4"
>
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
{!!ConnectionStatusElement && ConnectionStatusElement}

View File

@@ -1,14 +1,14 @@
import { useLoaderData, useRevalidator } from "react-router-dom";
import { LuMonitorSmartphone } from "react-icons/lu";
import { ArrowRightIcon } from "@heroicons/react/16/solid";
import { useInterval } from "usehooks-ts";
import DashboardNavbar from "@components/Header";
import { LinkButton } from "@components/Button";
import KvmCard from "@components/KvmCard";
import { useInterval } from "usehooks-ts";
import { checkAuth } from "@/main";
import { User } from "@/hooks/stores";
import EmptyCard from "@components/EmptyCard";
import KvmCard from "@components/KvmCard";
import { LinkButton } from "@components/Button";
import { User } from "@/hooks/stores";
import { checkAuth } from "@/main";
import { CLOUD_API } from "@/ui.config";
interface LoaderData {
@@ -16,7 +16,7 @@ interface LoaderData {
user: User;
}
export const loader = async () => {
const loader = async () => {
const user = await checkAuth();
try {
@@ -101,3 +101,5 @@ export default function DevicesRoute() {
</div>
);
}
DevicesRoute.loader = loader;

View File

@@ -61,13 +61,13 @@ export default function WelcomeLocalModeRoute() {
<Container>
<div className="flex items-center justify-center w-full h-full isolate">
<div className="max-w-xl space-y-8">
<div className="flex items-center justify-center opacity-0 animate-fadeIn">
<div className="flex items-center justify-center animate-fadeIn">
<img src={LogoWhiteIcon} alt="" className="-ml-4 h-[32px] hidden dark:block" />
<img src={LogoBlueIcon} alt="" className="-ml-4 h-[32px] dark:hidden" />
</div>
<div
className="space-y-2 text-center opacity-0 animate-fadeIn"
className="space-y-2 text-center animate-fadeIn"
style={{ animationDelay: "200ms" }}
>
<h1 className="text-4xl font-semibold text-black dark:text-white">Local Authentication Method</h1>
@@ -78,7 +78,7 @@ export default function WelcomeLocalModeRoute() {
<Form method="POST" className="space-y-8">
<div
className="grid grid-cols-1 gap-6 opacity-0 animate-fadeIn sm:grid-cols-2"
className="grid grid-cols-1 gap-6 animate-fadeIn sm:grid-cols-2"
style={{ animationDelay: "400ms" }}
>
{["password", "noPassword"].map(mode => (
@@ -120,7 +120,7 @@ export default function WelcomeLocalModeRoute() {
{actionData?.error && (
<p
className="text-sm text-center text-red-600 opacity-0 dark:text-red-400 animate-fadeIn"
className="text-sm text-center text-red-600 dark:text-red-400 animate-fadeIn"
style={{ animationDelay: "500ms" }}
>
{actionData.error}
@@ -128,7 +128,7 @@ export default function WelcomeLocalModeRoute() {
)}
<div
className="max-w-sm mx-auto opacity-0 animate-fadeIn"
className="max-w-sm mx-auto animate-fadeIn"
style={{ animationDelay: "500ms" }}
>
<Button
@@ -144,7 +144,7 @@ export default function WelcomeLocalModeRoute() {
</Form>
<p
className="max-w-md mx-auto text-xs text-center opacity-0 animate-fadeIn text-slate-500 dark:text-slate-400"
className="max-w-md mx-auto text-xs text-center animate-fadeIn text-slate-500 dark:text-slate-400"
style={{ animationDelay: "600ms" }}
>
You can always change your authentication method later in the settings.

View File

@@ -71,13 +71,13 @@ export default function WelcomeLocalPasswordRoute() {
<Container>
<div className="flex items-center justify-center w-full h-full isolate">
<div className="max-w-2xl space-y-8">
<div className="flex items-center justify-center opacity-0 animate-fadeIn">
<div className="flex items-center justify-center animate-fadeIn">
<img src={LogoWhiteIcon} alt="" className="-ml-4 h-[32px] hidden dark:block" />
<img src={LogoBlueIcon} alt="" className="-ml-4 h-[32px] dark:hidden" />
</div>
<div
className="space-y-2 text-center opacity-0 animate-fadeIn"
className="space-y-2 text-center animate-fadeIn"
style={{ animationDelay: "200ms" }}
>
<h1 className="text-4xl font-semibold text-black dark:text-white">Set a Password</h1>
@@ -90,7 +90,7 @@ export default function WelcomeLocalPasswordRoute() {
<Form method="POST" className="max-w-sm mx-auto space-y-4">
<div className="space-y-4">
<div
className="opacity-0 animate-fadeIn"
className="animate-fadeIn"
style={{ animationDelay: "400ms" }}
>
<InputFieldWithLabel
@@ -120,7 +120,7 @@ export default function WelcomeLocalPasswordRoute() {
/>
</div>
<div
className="opacity-0 animate-fadeIn"
className="animate-fadeIn"
style={{ animationDelay: "400ms" }}
>
<InputFieldWithLabel
@@ -137,7 +137,7 @@ export default function WelcomeLocalPasswordRoute() {
{actionData?.error && <p className="text-sm text-red-600">{}</p>}
<div
className="opacity-0 animate-fadeIn"
className="animate-fadeIn"
style={{ animationDelay: "600ms" }}
>
<Button
@@ -153,7 +153,7 @@ export default function WelcomeLocalPasswordRoute() {
</Fieldset>
<p
className="max-w-md text-xs text-center opacity-0 animate-fadeIn text-slate-500 dark:text-slate-400"
className="max-w-md text-xs text-center animate-fadeIn text-slate-500 dark:text-slate-400"
style={{ animationDelay: "800ms" }}
>
This password will be used to secure your device data and protect against

View File

@@ -47,13 +47,13 @@ export default function WelcomeRoute() {
<div className="max-w-3xl text-center">
<div className="space-y-8">
<div className="space-y-4">
<div className="flex items-center justify-center opacity-0 animate-fadeIn animation-delay-1000">
<div className="flex items-center justify-center animate-fadeIn animation-delay-1000">
<img src={LogoWhiteIcon} alt="JetKVM Logo" className="h-[32px] hidden dark:block" />
<img src={LogoBlueIcon} alt="JetKVM Logo" className="h-[32px] dark:hidden" />
</div>
<div
className="space-y-1 opacity-0 animate-fadeIn"
className="space-y-1 animate-fadeIn"
style={{ animationDelay: "1500ms" }}
>
<h1 className="text-4xl font-semibold text-black dark:text-white">
@@ -69,21 +69,21 @@ export default function WelcomeRoute() {
<img
src={DeviceImage}
alt="JetKVM Device"
className="animation-delay-0 max-w-md scale-[0.98] animate-fadeInScaleFloat opacity-0 transition-all duration-1000 ease-out"
className="animation-delay-0 max-w-md scale-[0.98] animate-fadeInScaleFloat transition-all duration-1000 ease-out"
/>
</div>
</div>
<div className="-mt-8 space-y-4">
<p
style={{ animationDelay: "2000ms" }}
className="max-w-lg mx-auto text-lg opacity-0 animate-fadeIn text-slate-700 dark:text-slate-300"
className="max-w-lg mx-auto text-lg animate-fadeIn text-slate-700 dark:text-slate-300"
>
JetKVM combines powerful hardware with intuitive software to provide a
seamless remote control experience.
</p>
<div
style={{ animationDelay: "2300ms" }}
className="opacity-0 animate-fadeIn"
className="animate-fadeIn"
>
<LinkButton
size="LG"