mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
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:
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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) ? (
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
{" "}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user