Compare commits

..

1 Commits

Author SHA1 Message Date
Techno Tim 9cc8e98742
Merge d84c00af94 into 340babac24 2025-05-14 16:12:17 -05:00
53 changed files with 1534 additions and 2252 deletions

66
ui/.eslintrc.cjs Normal file
View File

@ -0,0 +1,66 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic",
"plugin:react-hooks/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:import/recommended",
"prettier",
],
ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json", "./tsconfig.node.json"],
tsconfigRootDir: __dirname,
},
rules: {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"import/order": [
"error",
{
/**
* @description
*
* This keeps imports separate from one another, ensuring that imports are separated
* by their relative groups. As you move through the groups, imports become closer
* to the current file.
*
* @example
* ```
* import fs from 'fs';
*
* import package from 'npm-package';
*
* import xyz from '~/project-file';
*
* import index from '../';
*
* import sibling from './foo';
* ```
*/
groups: ["builtin", "external", "internal", "parent", "sibling"],
"newlines-between": "always",
},
],
},
settings: {
"import/resolver": {
alias: {
map: [
["@components", "./src/components"],
["@routes", "./src/routes"],
["@assets", "./src/assets"],
["@", "./src"],
],
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
},
},
},
};

View File

@ -1,93 +0,0 @@
const {
defineConfig,
globalIgnores,
} = require("eslint/config");
const globals = require("globals");
const {
fixupConfigRules,
} = require("@eslint/compat");
const tsParser = require("@typescript-eslint/parser");
const reactRefresh = require("eslint-plugin-react-refresh");
const js = require("@eslint/js");
const {
FlatCompat,
} = require("@eslint/eslintrc");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
module.exports = defineConfig([{
languageOptions: {
globals: {
...globals.browser,
},
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
project: ["./tsconfig.json", "./tsconfig.node.json"],
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true
}
},
},
extends: fixupConfigRules(compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic",
"plugin:react-hooks/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:import/recommended",
"prettier",
)),
plugins: {
"react-refresh": reactRefresh,
},
rules: {
"react-refresh/only-export-components": ["warn", {
allowConstantExport: true,
}],
"import/order": ["error", {
groups: ["builtin", "external", "internal", "parent", "sibling"],
"newlines-between": "always",
}],
},
settings: {
"react": {
"version": "detect"
},
"import/resolver": {
alias: {
map: [
["@components", "./src/components"],
["@routes", "./src/routes"],
["@assets", "./src/assets"],
["@", "./src"],
],
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
},
},
},
}, globalIgnores([
"**/dist",
"**/.eslintrc.cjs",
"**/tailwind.config.js",
"**/postcss.config.js",
])]);

2782
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"engines": {
"node": "22.15.0"
"node": "21.1.0"
},
"scripts": {
"dev": "./dev_device.sh",
@ -19,21 +19,21 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^2.2.3",
"@headlessui/react": "^2.2.2",
"@headlessui/tailwindcss": "^0.2.2",
"@heroicons/react": "^2.2.0",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"cva": "^1.0.0-beta.3",
"cva": "^1.0.0-beta.1",
"dayjs": "^1.11.13",
"eslint-import-resolver-alias": "^1.1.2",
"focus-trap-react": "^11.0.3",
"framer-motion": "^12.11.0",
"focus-trap-react": "^10.2.3",
"framer-motion": "^11.15.0",
"lodash.throttle": "^4.1.1",
"mini-svg-data-uri": "^1.4.4",
"react": "^19.1.0",
@ -42,29 +42,24 @@
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0",
"react-router-dom": "^6.22.3",
"react-simple-keyboard": "^3.8.72",
"react-simple-keyboard": "^3.8.71",
"react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10",
"recharts": "^2.15.3",
"tailwind-merge": "^3.3.0",
"tailwind-merge": "^2.5.5",
"usehooks-ts": "^3.1.1",
"validator": "^13.15.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@eslint/compat": "^1.2.9",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.26.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.6",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.6",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@types/semver": "^7.7.0",
"@types/validator": "^13.15.0",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.26.0",
@ -73,13 +68,12 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.1.0",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.1.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite": "^5.2.0",
"vite-tsconfig-paths": "^5.1.4"
}
}

View File

@ -1,5 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -37,7 +37,7 @@ export default function AuthLayout({
<>
<GridBackground />
<div className="grid min-h-screen grid-rows-(--grid-layout)">
<div className="grid min-h-screen grid-rows-layout">
<SimpleNavbar
logoHref="/"
actionElement={

View File

@ -16,7 +16,7 @@ const sizes = {
const themes = {
primary: cx(
// Base styles
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow-sm",
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow",
// Hover states
"group-hover:bg-blue-800",
// Active states
@ -24,7 +24,7 @@ const themes = {
),
danger: cx(
// Base styles
"bg-red-600 text-white border-red-700 shadow-xs shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
"bg-red-600 text-white border-red-700 shadow-sm shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
// Hover states
"group-hover:bg-red-700 group-hover:border-red-800 dark:group-hover:bg-red-700 dark:group-hover:border-red-600",
// Active states
@ -34,7 +34,7 @@ const themes = {
),
light: cx(
// Base styles
"bg-white text-black border-slate-800/30 shadow-xs dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
"bg-white text-black border-slate-800/30 shadow dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
// Hover states
"group-hover:bg-blue-50/80 dark:group-hover:bg-slate-700",
// Active states
@ -44,7 +44,7 @@ const themes = {
),
lightDanger: cx(
// Base styles
"bg-white text-black border-red-400/60 shadow-xs",
"bg-white text-black border-red-400/60 shadow-sm",
// Hover states
"group-hover:bg-red-50/80",
// Active states
@ -56,7 +56,7 @@ const themes = {
// Base styles
"bg-white/0 text-black border-transparent dark:text-white",
// Hover states
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow-sm dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
// Active states
"group-active:bg-slate-100/80",
),
@ -65,15 +65,15 @@ const themes = {
const btnVariants = cva({
base: cx(
// Base styles
"border rounded-sm select-none",
"border rounded select-none",
// Size classes
"justify-center items-center shrink-0",
// Transition classes
"outline-hidden transition-all duration-200",
"outline-none transition-all duration-200",
// Text classes
"font-display text-center font-medium leading-tight",
// States
"group-focus:outline-hidden group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
"group-focus:outline-none group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
"group-disabled:opacity-50 group-disabled:pointer-events-none",
),
@ -175,7 +175,7 @@ type ButtonPropsType = Pick<
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
const classes = cx(
"group outline-hidden",
"group outline-none",
props.fullWidth ? "w-full" : "",
loading ? "pointer-events-none" : "",
);
@ -215,7 +215,7 @@ type LinkPropsType = Pick<LinkProps, "to"> &
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
const classes = cx(
"group outline-hidden",
"group outline-none",
props.disabled ? "pointer-events-none !opacity-70" : "",
props.fullWidth ? "w-full" : "",
props.loading ? "pointer-events-none" : "",
@ -241,7 +241,7 @@ type LabelPropsType = Pick<HTMLLabelElement, "htmlFor"> &
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
export const LabelButton = ({ htmlFor, ...props }: LabelPropsType) => {
const classes = cx(
"group outline-hidden block cursor-pointer",
"group outline-none block cursor-pointer",
props.disabled ? "pointer-events-none !opacity-70" : "",
props.fullWidth ? "w-full" : "",
props.loading ? "pointer-events-none" : "",

View File

@ -30,7 +30,7 @@ const Card = forwardRef<HTMLDivElement, CardPropsType>(({ children, className },
<div
ref={ref}
className={cx(
"w-full rounded-sm border-none bg-white shadow-xs 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,
)}
>

View File

@ -15,7 +15,7 @@ const checkboxVariants = cva({
"block rounded",
// Colors
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors",
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 text-blue-700 dark:text-blue-500 transition-colors",
// Hover
"hover:bg-slate-200/50 dark:hover:bg-slate-700/50",
@ -24,7 +24,7 @@ const checkboxVariants = cva({
"active:bg-slate-200 dark:active:bg-slate-700",
// Focus
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-hidden focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
// Disabled
"disabled:pointer-events-none disabled:opacity-30",
@ -41,9 +41,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckBoxProps>(function Checkbox(
ref,
) {
const classes = checkboxVariants({ size });
return (
<input ref={ref} {...props} type="checkbox" className={clsx(classes, className)} />
);
return <input ref={ref} {...props} type="checkbox" className={clsx(classes, className)} />;
});
Checkbox.displayName = "Checkbox";

View File

@ -58,7 +58,7 @@ export function Combobox({
<HeadlessCombobox onChange={onChange} {...otherProps}>
{() => (
<>
<Card className="w-auto !border border-solid !border-slate-800/30 shadow-xs outline-0 dark:!border-slate-300/30">
<Card className="w-auto !border border-solid !border-slate-800/30 shadow outline-0 dark:!border-slate-300/30">
<ComboboxInput
ref={inputRef}
className={clsx(

View File

@ -30,7 +30,7 @@ export default function EmptyCard({
<div className="max-w-[90%] space-y-1.5 text-center md:max-w-[60%]">
<div className="space-y-2">
{IconElm && (
<IconElm className="mx-auto h-5 w-5 text-blue-600 dark:text-blue-600" />
<IconElm className="mx-auto h-6 w-6 text-blue-600 dark:text-blue-400" />
)}
<h4 className="text-base font-bold leading-none text-black dark:text-white">
{headline}

View File

@ -1,8 +1,8 @@
export default function GridBackground() {
return (
<div className="absolute isolate h-screen w-screen overflow-hidden opacity-60">
<div className="absolute w-screen h-screen overflow-hidden isolate opacity-60">
<svg
className="absolute inset-x-0 top-0 -z-10 h-full w-full mask-radial-[32rem_32rem] mask-radial-from-white mask-radial-to-transparent mask-radial-at-center stroke-gray-300 dark:stroke-slate-300/20"
className="absolute inset-x-0 top-0 -z-10 h-[64rem] w-full stroke-gray-300 [mask-image:radial-gradient(32rem_32rem_at_center,white,transparent)] dark:stroke-slate-300/20"
aria-hidden="true"
>
<defs>

View File

@ -1,11 +1,12 @@
import { useCallback } from "react";
import { Fragment, 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 { Menu, MenuButton } from "@headlessui/react";
import { LuMonitorSmartphone } from "react-icons/lu";
import Container from "@/components/Container";
import Card from "@/components/Card";
import { cx } from "@/cva.config";
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
import LogoBlueIcon from "@/assets/logo-blue.svg";
import LogoWhiteIcon from "@/assets/logo-white.svg";
@ -16,7 +17,7 @@ import { CLOUD_API, DEVICE_API } from "@/ui.config";
import api from "../api";
import { isOnDevice } from "../main";
import { LinkButton } from "./Button";
import { Button, LinkButton } from "./Button";
interface NavbarProps {
isLoggedIn: boolean;
@ -50,12 +51,8 @@ export default function DashboardNavbar({
const usbState = useHidStore(state => state.usbState);
// for testing
//userEmail = "user@example.org";
//picture = "https://placehold.co/32x32"
return (
<div className="w-full border-b border-b-slate-800/20 bg-white select-none dark:border-b-slate-300/20 dark:bg-slate-900">
<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">
@ -81,82 +78,86 @@ export default function DashboardNavbar({
</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-stretch 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}
{showConnectionStatus && (
<div className="hidden items-center gap-x-2 md:flex">
<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] self-center border-none bg-slate-800/20 dark:bg-slate-300/20" />
<div className="relative inline-block text-left">
<Menu>
<MenuButton className="h-full">
<Button className="flex h-full items-center gap-x-3 rounded-md border border-slate-800/20 bg-white px-2 py-1.5 dark:border-slate-600 dark:bg-slate-800 dark:text-white">
{picture ? (
/>
</div>
</div>
)}
{isLoggedIn ? (
<>
<hr className="h-[20px] w-[1px] border-none bg-slate-800/20 dark:bg-slate-300/20" />
<Menu as="div" className="relative inline-block text-left">
<div>
<MenuButton as={Fragment}>
<Button
theme="blank"
size="SM"
text={
<>
{picture ? <></> : userEmail}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-slate-900 dark:text-white" />
</>
}
LeadingIcon={({ className }) =>
picture && (
<img
src={picture}
alt="Avatar"
className="size-6 rounded-full border-2 border-transparent transition-colors group-hover:border-blue-700"
className={cx(
className,
"h-8 w-8 rounded-full border-2 border-transparent transition-colors group-hover:border-blue-700",
)}
/>
) : userEmail ? (
<span className="font-display max-w-[200px] truncate text-sm/6 font-semibold">
{userEmail}
</span>
) : null}
<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 p-px focus:outline-hidden data-closed:opacity-0"
>
<MenuItem>
<Card className="overflow-hidden">
{userEmail && (
<div className="space-y-1 p-1 dark:text-white">
<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="font-display max-w-[200px] truncate text-sm font-semibold">
{userEmail}
</div>
</div>
)
}
/>
</MenuButton>
</div>
<Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right focus:outline-none">
<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">
<Menu.Item>
<div className="p-2">
<div className="font-display text-xs">Logged in as</div>
<div className="w-[200px] truncate font-display text-sm font-semibold">
{userEmail}
</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>
</Menu.Item>
</div>
)}
<div>
<Menu.Item>
<div onClick={onLogout}>
<button className="block w-full">
<div className="flex items-center gap-x-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
<ArrowLeftEndOnRectangleIcon className="h-4 w-4" />
<div className="font-display">Log out</div>
</div>
</button>
</div>
</Card>
</MenuItem>
</MenuItems>
</Menu>
</div>
</>
) : null}
</div>
</Menu.Item>
</div>
</div>
</Card>
</Menu.Items>
</Menu>
</>
) : null}
</div>
</div>
</div>

View File

@ -44,7 +44,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
"[&:has(:user-invalid)]:ring-2 [&:has(:user-invalid)]:ring-red-600 [&:has(:user-invalid)]:ring-offset-2",
// Focus Within
"focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-hidden focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2",
"focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2",
// Disabled Within
"disabled-within:pointer-events-none disabled-within:select-none disabled-within:bg-slate-50 dark:disabled-within:bg-slate-800 disabled-within:text-slate-500/80",

View File

@ -113,7 +113,7 @@ export default function KvmCard({
transition
className="data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
>
<Card className="absolute right-0 z-10 w-56 px-1 mt-2 transition origin-top-right ring-1 ring-black/50 focus:outline-hidden">
<Card className="absolute right-0 z-10 w-56 px-1 mt-2 transition origin-top-right ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="divide-y divide-slate-800/20 dark:divide-slate-300/20">
<MenuItem>
<div>

View File

@ -7,7 +7,7 @@ export default function LoadingSpinner({
}) {
return (
<svg
className={clsx(className, "shrink-0 animate-spin p-[2px]")}
className={clsx(className, "flex-shrink-0 animate-spin p-[2px]")}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"

View File

@ -63,7 +63,7 @@ export const SelectMenuBasic = React.forwardRef<HTMLSelectElement, SelectMenuPro
)}
>
{label && <FieldLabel label={label} id={id} as="span" />}
<Card className="w-auto !border border-solid !border-slate-800/30 shadow-xs outline-0 dark:!border-slate-300/30">
<Card className="w-auto !border border-solid !border-slate-800/30 shadow outline-0 dark:!border-slate-300/30">
<select
ref={ref}
name={name}
@ -72,7 +72,7 @@ export const SelectMenuBasic = React.forwardRef<HTMLSelectElement, SelectMenuPro
classes,
// General styling
"block w-full cursor-pointer rounded-sm border-none py-0 font-medium shadow-none outline-0 transition duration-300",
"block w-full cursor-pointer rounded border-none py-0 font-medium shadow-none outline-0 transition duration-300",
// Hover
"hover:bg-blue-50/80 active:bg-blue-100/60 disabled:hover:bg-white",

View File

@ -44,7 +44,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
return (
<div
className={cx(
"rounded-md border border-blue-800 bg-blue-700 px-2 py-1 font-medium text-white shadow-xs dark:border-blue-300",
"rounded-md border border-blue-800 bg-blue-700 px-2 py-1 font-medium text-white shadow-sm dark:border-blue-300",
textStyle,
)}
key={`${i}-${currStepIdx}`}

View File

@ -17,7 +17,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
className={cx(
"relative w-full",
"invalid-within::ring-2 invalid-within::ring-red-600 invalid-within::ring-offset-2",
"focus-within:border-slate-300 focus-within:outline-hidden focus-within:ring-1 focus-within:ring-blue-700 dark:focus-within:border-slate-600",
"focus-within:border-slate-300 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 dark:focus-within:border-slate-600",
)}
>
<textarea
@ -25,7 +25,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
{...props}
id="asd"
className={clsx(
"block w-full rounded-sm border-transparent bg-transparent text-black placeholder:text-slate-300 focus:ring-0 disabled:pointer-events-none disabled:select-none disabled:bg-slate-50 disabled:text-slate-300 dark:text-white dark:placeholder:text-slate-500 dark:disabled:bg-slate-800 sm:text-sm",
"block w-full rounded border-transparent bg-transparent text-black placeholder:text-slate-300 focus:ring-0 disabled:pointer-events-none disabled:select-none disabled:bg-slate-50 disabled:text-slate-300 dark:text-white dark:placeholder:text-slate-500 dark:disabled:bg-slate-800 sm:text-sm",
props.className,
)}
/>

View File

@ -14,7 +14,7 @@ interface OverlayContentProps {
}
function OverlayContent({ children }: OverlayContentProps) {
return (
<GridCard cardClassName="h-full pointer-events-auto !outline-hidden">
<GridCard cardClassName="h-full pointer-events-auto !outline-none">
<div className="flex h-full w-full flex-col items-center justify-center rounded-md border border-slate-800/30 dark:border-slate-300/20">
{children}
</div>
@ -377,7 +377,7 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
>
<div>
<Card className="rounded-b-none shadow-none !outline-0">
<div className="flex items-center justify-between border border-slate-800/50 px-4 py-2 outline-0 backdrop-blur-xs dark:border-slate-300/20 dark:bg-slate-800">
<div className="flex items-center justify-between border border-slate-800/50 px-4 py-2 outline-0 backdrop-blur-sm dark:border-slate-300/20 dark:bg-slate-800">
<div className="flex items-center space-x-2">
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
<span className="text-sm text-black dark:text-white">

View File

@ -143,16 +143,6 @@ function KeyboardWrapper() {
return;
}
if (key === "CtrlAltBackspace") {
sendKeyboardEvent(
[keys["Backspace"]],
[modifiers["ControlLeft"], modifiers["AltLeft"]],
);
setTimeout(resetKeyboardState, 100);
return;
}
if (isKeyShift || isKeyCaps) {
toggleLayout();
@ -267,13 +257,13 @@ function KeyboardWrapper() {
buttonTheme={[
{
class: "combination-key",
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
buttons: "CtrlAltDelete AltMetaEscape",
},
]}
display={keyDisplayMap}
layout={{
default: [
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
"CtrlAltDelete AltMetaEscape",
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash",
@ -282,7 +272,7 @@ function KeyboardWrapper() {
"ControlLeft AltLeft MetaLeft Space MetaRight AltRight",
],
shift: [
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
"CtrlAltDelete AltMetaEscape",
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
@ -292,7 +282,7 @@ function KeyboardWrapper() {
],
}}
disableButtonHold={true}
syncInstanceInputs={true}
mergeDisplay={true}
debug={false}
/>
@ -300,25 +290,34 @@ function KeyboardWrapper() {
<Keyboard
baseClass="simple-keyboard-control"
theme="simple-keyboard hg-theme-default hg-layout-default"
layoutName={layoutName}
onKeyPress={onKeyDown}
display={keyDisplayMap}
layout={{
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"],
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"],
default: ["Home Pageup", "Delete End Pagedown"],
}}
display={{
Home: "home",
Pageup: "pageup",
Delete: "delete",
End: "end",
Pagedown: "pagedown",
}}
syncInstanceInputs={true}
onKeyPress={onKeyDown}
mergeDisplay={true}
debug={false}
/>
<Keyboard
baseClass="simple-keyboard-arrows"
theme="simple-keyboard hg-theme-default hg-layout-default"
onKeyPress={onKeyDown}
display={keyDisplayMap}
display={{
ArrowLeft: "←",
ArrowRight: "→",
ArrowUp: "↑",
ArrowDown: "↓",
}}
layout={{
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
}}
syncInstanceInputs={true}
onKeyPress={onKeyDown}
debug={false}
/>
</div>

View File

@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useResizeObserver } from "usehooks-ts";
import {
useDeviceSettingsStore,
@ -11,6 +10,7 @@ import {
useVideoStore,
} from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { useResizeObserver } from "usehooks-ts";
import { cx } from "@/cva.config";
import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
@ -673,7 +673,7 @@ export default function WebRTCVideo() {
]);
return (
<div className="grid h-full w-full grid-rows-(--grid-layout)">
<div className="grid h-full w-full grid-rows-layout">
<div className="flex min-h-[39.5px] flex-col">
<div className="flex flex-col">
<fieldset
@ -699,7 +699,7 @@ export default function WebRTCVideo() {
<div className="flex h-full flex-col">
<div className="relative flex-grow overflow-hidden">
<div className="flex h-full flex-col">
<div className="grid flex-grow grid-rows-(--grid-bodyFooter) overflow-hidden">
<div className="grid flex-grow grid-rows-bodyFooter overflow-hidden">
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
<div className="relative flex h-full w-full items-center justify-center">
<div className="relative inline-block">
@ -724,7 +724,7 @@ export default function WebRTCVideo() {
hdmiError ||
peerConnectionState !== "connected",
"!opacity-60": showPointerLockBar,
"animate-slideUpFade border border-slate-800/30 shadow-xs dark:border-slate-300/20":
"animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
isPlaying,
},
)}
@ -732,7 +732,7 @@ export default function WebRTCVideo() {
{peerConnection?.connectionState == "connected" && (
<div
style={{ animationDuration: "500ms" }}
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center"
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center opacity-0"
>
<div className="relative h-full w-full rounded-md">
<LoadingVideoOverlay show={isVideoLoading} />

View File

@ -107,7 +107,7 @@ export function ATXPowerControl() {
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
</Card>
) : (
<Card className="h-[120px] animate-fadeIn">
<Card className="h-[120px] animate-fadeIn opacity-0">
<div className="space-y-4 p-3">
{/* Control Buttons */}
<div className="flex items-center space-x-2">

View File

@ -63,7 +63,7 @@ export function DCPowerControl() {
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
</Card>
) : (
<Card className="h-[160px] animate-fadeIn">
<Card className="h-[160px] animate-fadeIn opacity-0">
<div className="space-y-4 p-3">
{/* Power Controls */}
<div className="flex items-center space-x-2">

View File

@ -58,7 +58,7 @@ export function SerialConsole() {
description="Configure your serial console settings"
/>
<Card className="animate-fadeIn">
<Card className="animate-fadeIn opacity-0">
<div className="space-y-4 p-3">
{/* Open Console Button */}
<div className="flex items-center">

View File

@ -84,7 +84,7 @@ export default function ExtensionPopover() {
return (
<GridCard>
<div className="space-y-4 p-4 py-3">
<div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="grid h-full grid-rows-headerBody">
<div className="space-y-4">
{activeExtension ? (
// Extension Control View
@ -92,7 +92,7 @@ export default function ExtensionPopover() {
{renderActiveExtension()}
<div
className="flex animate-fadeIn items-center justify-end space-x-2"
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",
@ -113,7 +113,7 @@ export default function ExtensionPopover() {
title="Extensions"
description="Load and manage your extensions"
/>
<Card className="animate-fadeIn">
<Card className="animate-fadeIn opacity-0">
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
{AVAILABLE_EXTENSIONS.map(extension => (
<div

View File

@ -194,7 +194,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
return (
<GridCard>
<div className="space-y-4 p-4 py-3">
<div ref={ref} className="grid h-full grid-rows-(--grid-headerBody)">
<div ref={ref} className="grid h-full grid-rows-headerBody">
<div className="h-full space-y-4">
<div className="space-y-4">
<SettingsPageHeader
@ -214,7 +214,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
) : null}
<div
className="animate-fadeIn space-y-2"
className="animate-fadeIn space-y-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -289,7 +289,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
{!remoteVirtualMediaState && (
<div
className="flex animate-fadeIn items-center justify-end space-x-2"
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -74,7 +74,7 @@ export default function PasteModal() {
return (
<GridCard>
<div className="space-y-4 p-4 py-3">
<div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="grid h-full grid-rows-headerBody">
<div className="h-full space-y-4">
<div className="space-y-4">
<SettingsPageHeader
@ -83,7 +83,7 @@ export default function PasteModal() {
/>
<div
className="animate-fadeIn space-y-2"
className="animate-fadeIn space-y-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -137,7 +137,7 @@ export default function PasteModal() {
</div>
</div>
<div
className="flex animate-fadeIn items-center justify-end gap-x-2"
className="flex animate-fadeIn items-center justify-end gap-x-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -26,7 +26,7 @@ export default function AddDeviceForm({
return (
<div className="space-y-4">
<div
className="animate-fadeIn space-y-4"
className="animate-fadeIn space-y-4 opacity-0"
style={{
animationDuration: "0.5s",
animationFillMode: "forwards",
@ -73,7 +73,7 @@ export default function AddDeviceForm({
/>
</div>
<div
className="flex animate-fadeIn items-center justify-end space-x-2"
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -28,7 +28,7 @@ export default function DeviceList({
}: DeviceListProps) {
return (
<div className="space-y-4">
<Card className="animate-fadeIn">
<Card className="animate-fadeIn opacity-0">
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
{storedDevices.map((device, index) => (
<div key={index} className="flex items-center justify-between gap-x-2 p-3">
@ -63,7 +63,7 @@ export default function DeviceList({
</div>
</Card>
<div
className="flex animate-fadeIn items-center justify-end space-x-2"
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -13,7 +13,7 @@ export default function EmptyStateCard({
}) {
return (
<div className="select-none space-y-4">
<Card className="animate-fadeIn">
<Card className="animate-fadeIn opacity-0">
<div className="flex items-center justify-center py-8 text-center">
<div className="space-y-3">
<div className="space-y-1">
@ -35,7 +35,7 @@ export default function EmptyStateCard({
</div>
</Card>
<div
className="flex animate-fadeIn items-center justify-end space-x-2"
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -102,7 +102,7 @@ export default function WakeOnLanModal() {
return (
<GridCard>
<div className="space-y-4 p-4 py-3">
<div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="grid h-full grid-rows-headerBody">
<div className="space-y-4">
<SettingsPageHeader
title="Wake On LAN"

View File

@ -99,7 +99,7 @@ export default function ConnectionStatsSidebar() {
}, 500);
return (
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
<div className="grid h-full grid-rows-headerBody shadow-sm">
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
<div className="space-y-4">

View File

@ -1,11 +1,6 @@
@import "tailwindcss";
@config "../tailwind.config.js";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@plugin "@headlessui/tailwindcss";
/* Dark mode uses CSS selector instead of prefers-color-scheme */
@custom-variant dark (&:where(.dark, .dark *));
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
@apply scroll-smooth;
@ -18,128 +13,6 @@ body {
overflow: auto;
}
@theme {
--font-sans: "Circular", sans-serif;
--font-display: "Circular", sans-serif;
--font-serif: "Circular", serif;
--font-mono: "Source Code Pro Variable", monospace;
--grid-layout: auto 1fr auto;
--grid-headerBody: auto 1fr;
--grid-bodyFooter: 1fr auto;
--grid-sidebar: 1fr minmax(360px, 25%);
--breakpoint-xs: 480px;
--breakpoint-2xl: 1440px;
--breakpoint-3xl: 1920px;
--breakpoint-4xl: 2560px;
/* Migrated animations */
--animate-enter: enter 0.2s ease-out;
--animate-leave: leave 0.15s ease-in forwards;
--animate-fadeInScale: fadeInScale 1s ease-out forwards;
--animate-fadeInScaleFloat:
fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite;
--animate-fadeIn: fadeIn 1s ease-out forwards;
--animate-slideUpFade: slideUpFade 1s ease-out forwards;
--container-8xl: 88rem;
--container-9xl: 96rem;
--container-10xl: 104rem;
--container-11xl: 112rem;
--container-12xl: 120rem;
/* Migrated keyframes */
@keyframes enter {
0% {
opacity: 0;
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes leave {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.9);
}
}
@keyframes fadeInScale {
0% {
opacity: 0;
transform: scale(0.98);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeInScaleFloat {
0% {
opacity: 0;
transform: scale(0.98) translateY(10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(10px);
}
70% {
opacity: 0.8;
transform: translateY(1px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUpFade {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
}
@utility max-width-* {
max-width: --modifier(--container- *, [length], [ *]);
}
/* Ensure there is not a `ms` and ms -> `...)ms` */
@utility animation-delay-* {
animation-delay: --value(integer) ms;
}
@property --grid-color-start {
syntax: "<color>";
initial-value: theme("colors.blue.50/10");
@ -177,7 +50,7 @@ video::-webkit-media-controls {
}
.hg-theme-default .hg-button {
@apply border !border-b border-slate-800/25 !border-b-slate-800/25 !shadow-xs;
@apply border !border-b border-slate-800/25 !border-b-slate-800/25 !shadow-sm;
}
.hg-theme-default .hg-button span {
@ -301,7 +174,7 @@ video::-webkit-media-controls {
}
.hg-theme-default .hg-row .combination-key {
@apply inline-flex !h-auto !w-auto grow-0 py-1 text-xs;
@apply inline-flex !h-auto !w-auto flex-grow-0 py-1 text-xs;
}
.hg-theme-default .hg-row:has(.combination-key) {

View File

@ -86,21 +86,16 @@ export const keys = {
NumpadAdd: 0x57,
NumpadDivide: 0x54,
NumpadEnter: 0x58,
NumpadEqual: 0x67,
NumpadMultiply: 0x55,
NumpadSubtract: 0x56,
NumpadDecimal: 0x63,
PageDown: 0x4e,
PageUp: 0x4b,
Period: 0x37,
PrintScreen: 0x46,
Pause: 0x48,
Quote: 0x34,
ScrollLock: 0x47,
Semicolon: 0x33,
Slash: 0x38,
Space: 0x2c,
SystemRequest: 0x9a,
Tab: 0x2b,
} as Record<string, number>;
@ -205,13 +200,6 @@ export const chars = {
"\n": { key: "Enter", shift: false },
Enter: { key: "Enter", shift: false },
Tab: { key: "Tab", shift: false },
PrintScreen: { key: "Prt Sc", shift: false },
SystemRequest: { key: "Prt Sc", shift: true },
ScrollLock: { key: "ScrollLock", shift: false},
Pause: { key: "Pause", shift: false },
Break: { key: "Pause", shift: true },
Insert: { key: "Insert", shift: false },
Delete: { key: "Delete", shift: false },
} as Record<string, { key: string | number; shift: boolean }>;
export const modifiers = {
@ -239,7 +227,6 @@ export const modifierDisplayMap: Record<string, string> = {
export const keyDisplayMap: Record<string, string> = {
CtrlAltDelete: "Ctrl + Alt + Delete",
AltMetaEscape: "Alt + Meta + Escape",
CtrlAltBackspace: "Ctrl + Alt + Backspace",
Escape: "esc",
Tab: "tab",
Backspace: "backspace",
@ -253,12 +240,11 @@ export const keyDisplayMap: Record<string, string> = {
MetaLeft: "meta",
MetaRight: "meta",
Space: " ",
Insert: "insert",
Home: "home",
PageUp: "page up",
PageUp: "pageup",
Delete: "delete",
End: "end",
PageDown: "page down",
PageDown: "pagedown",
ArrowLeft: "←",
ArrowRight: "→",
ArrowUp: "↑",
@ -272,45 +258,22 @@ export const keyDisplayMap: Record<string, string> = {
KeyU: "u", KeyV: "v", KeyW: "w", KeyX: "x", KeyY: "y",
KeyZ: "z",
// Capital letters
"(KeyA)": "A", "(KeyB)": "B", "(KeyC)": "C", "(KeyD)": "D", "(KeyE)": "E",
"(KeyF)": "F", "(KeyG)": "G", "(KeyH)": "H", "(KeyI)": "I", "(KeyJ)": "J",
"(KeyK)": "K", "(KeyL)": "L", "(KeyM)": "M", "(KeyN)": "N", "(KeyO)": "O",
"(KeyP)": "P", "(KeyQ)": "Q", "(KeyR)": "R", "(KeyS)": "S", "(KeyT)": "T",
"(KeyU)": "U", "(KeyV)": "V", "(KeyW)": "W", "(KeyX)": "X", "(KeyY)": "Y",
"(KeyZ)": "Z",
// Numbers
Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4", Digit5: "5",
Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9", Digit0: "0",
// Shifted Numbers
"(Digit1)": "!", "(Digit2)": "@", "(Digit3)": "#", "(Digit4)": "$", "(Digit5)": "%",
"(Digit6)": "^", "(Digit7)": "&", "(Digit8)": "*", "(Digit9)": "(", "(Digit0)": ")",
// Symbols
Minus: "-",
"(Minus)": "_",
Equal: "=",
"(Equal)": "+",
BracketLeft: "[",
"(BracketLeft)": "{",
BracketRight: "]",
"(BracketRight)": "}",
Backslash: "\\",
"(Backslash)": "|",
Semicolon: ";",
"(Semicolon)": ":",
Quote: "'",
"(Quote)": "\"",
Comma: ",",
"(Comma)": "<",
Period: ".",
"(Period)": ">",
Slash: "/",
"(Slash)": "?",
Backquote: "`",
"(Backquote)": "~",
IntlBackslash: "\\",
// Function keys
@ -324,11 +287,5 @@ export const keyDisplayMap: Record<string, string> = {
Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8",
Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -",
NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .",
NumpadEqual: "Num =", NumpadEnter: "Num Enter",
NumLock: "Num Lock",
// Modals
PrintScreen: "prt sc", ScrollLock: "scr lk", Pause: "pause",
"(PrintScreen)": "sys rq", "(Pause)": "break",
SystemRequest: "sys rq", Break: "break"
NumpadEnter: "Num Enter"
};

View File

@ -17,7 +17,7 @@ import AdoptRoute from "@routes/adopt";
import SignupRoute from "@routes/signup";
import LoginRoute from "@routes/login";
import SetupRoute from "@routes/devices.$id.setup";
import DevicesRoute from "@routes/devices";
import DevicesRoute, { loader as DeviceListLoader } from "@routes/devices";
import DeviceRoute, { LocalDevice } from "@routes/devices.$id";
import Card from "@components/Card";
import DevicesAlreadyAdopted from "@routes/devices.already-adopted";
@ -36,7 +36,7 @@ import SettingsKeyboardMouseRoute from "./routes/devices.$id.settings.mouse";
import api from "./api";
import * as SettingsIndexRoute from "./routes/devices.$id.settings._index";
import SettingsAdvancedRoute from "./routes/devices.$id.settings.advanced";
import SettingsAccessIndexRoute from "./routes/devices.$id.settings.access._index";
import * as SettingsAccessIndexRoute from "./routes/devices.$id.settings.access._index";
import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
@ -166,7 +166,7 @@ if (isOnDevice) {
children: [
{
index: true,
element: <SettingsAccessIndexRoute />,
element: <SettingsAccessIndexRoute.default />,
loader: SettingsAccessIndexRoute.loader,
},
{
@ -291,7 +291,7 @@ if (isOnDevice) {
children: [
{
index: true,
element: <SettingsAccessIndexRoute />,
element: <SettingsAccessIndexRoute.default />,
loader: SettingsAccessIndexRoute.loader,
},
{
@ -341,10 +341,7 @@ if (isOnDevice) {
loader: DeviceIdRename.loader,
action: DeviceIdRename.action,
},
{
path: "devices",
element: <DevicesRoute />,
loader: DevicesRoute.loader },
{ path: "devices", element: <DevicesRoute />, loader: DeviceListLoader },
],
},
],
@ -359,7 +356,7 @@ document.addEventListener("DOMContentLoaded", () => {
<Notifications
toastOptions={{
className:
"rounded-sm border-none bg-white text-black shadow-sm outline-1 outline-slate-800/30",
"rounded border-none bg-white text-black shadow outline outline-1 outline-slate-800/30",
}}
max={2}
/>

View File

@ -71,13 +71,12 @@ export default function DevicesIdDeregister() {
const error = useActionData() as { message: string };
return (
<div className="grid min-h-screen grid-rows-(--grid-layout)">
<div className="grid min-h-screen grid-rows-layout">
<DashboardNavbar
isLoggedIn={!!user}
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")}
className={cx("animate-fadeIn opacity-0")}
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-xs transition-all duration-100 hover:shadow-md dark:bg-slate-800",
"w-full min-w-[250px] cursor-pointer bg-white shadow-sm 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"
className="flex animate-fadeIn justify-end opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",
@ -437,7 +437,7 @@ function BrowserFileView({
className="block cursor-pointer select-none"
>
<div
className="group animate-fadeIn"
className="group animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
}}
@ -483,7 +483,7 @@ function BrowserFileView({
</div>
<div
className="flex w-full animate-fadeIn items-end justify-between"
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -578,7 +578,7 @@ function UrlView({
/>
<div
className="animate-fadeIn"
className="animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
}}
@ -593,7 +593,7 @@ function UrlView({
/>
</div>
<div
className="flex w-full animate-fadeIn items-end justify-between"
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
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"
className="animate-fadeIn opacity-0"
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"
className="w-full animate-fadeIn opacity-0"
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"
className="flex animate-fadeIn items-end justify-between opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.15s",
@ -914,7 +914,7 @@ function DeviceFileView({
</div>
) : (
<div
className="flex animate-fadeIn items-end justify-end"
className="flex animate-fadeIn items-end justify-end opacity-0"
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"
className="animate-fadeIn space-y-2 opacity-0"
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-xs bg-slate-200 dark:bg-slate-700">
<div className="h-3.5 w-full overflow-hidden rounded-sm bg-slate-200 dark:bg-slate-700">
<div
className="h-full rounded-xs bg-blue-700 transition-all duration-300 ease-in-out dark:bg-blue-500"
className="h-full rounded-sm 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"
className="w-full animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.25s",
@ -1251,7 +1251,7 @@ function UploadFileView({
}
/>
<div
className="animate-fadeIn space-y-2"
className="animate-fadeIn space-y-2 opacity-0"
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 dark:text-red-400"
className="mt-2 animate-fadeIn truncate text-sm text-red-600 opacity-0 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"
className="flex w-full animate-fadeIn items-end opacity-0"
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

@ -75,13 +75,12 @@ export default function DeviceIdRename() {
const error = useActionData() as { message: string };
return (
<div className="grid min-h-screen grid-rows-(--grid-layout)">
<div className="grid min-h-screen grid-rows-layout">
<DashboardNavbar
isLoggedIn={!!user}
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;
}
const loader = async () => {
export const loader = async () => {
if (isOnDevice) {
const status = await api
.GET(`${DEVICE_API}/device`)
@ -468,5 +468,3 @@ export default function SettingsAccessIndexRoute() {
</div>
);
}
SettingsAccessIndexRoute.loader = loader;

View File

@ -168,14 +168,14 @@ export default function SettingsMacrosRoute() {
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
{macro.name}
</h3>
<p className="mt-1 ml-4 overflow-hidden text-xs text-slate-500 dark:text-slate-400">
<p className="ml-4 mt-1 overflow-hidden text-xs text-slate-500 dark:text-slate-400">
<span className="flex flex-col items-start gap-1">
{macro.steps.map((step, stepIndex) => {
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
return (
<span key={stepIndex} className="inline-flex items-center">
<StepIcon className="mr-1 h-3 w-3 shrink-0 text-slate-400 dark:text-slate-500" />
<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">
{(Array.isArray(step.modifiers) &&
step.modifiers.length > 0) ||
@ -338,7 +338,6 @@ export default function SettingsMacrosRoute() {
<EmptyCard
IconElm={LuCommand}
headline="Create Your First Macro"
description="Combine keystrokes into a single action"
BtnElm={
<Button
size="SM"

View File

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

View File

@ -12,16 +12,15 @@ 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

@ -56,7 +56,7 @@ export default function SetupRoute() {
return (
<>
<GridBackground />
<div className="grid min-h-screen grid-rows-(--grid-layout)">
<div className="grid min-h-screen grid-rows-layout">
<SimpleNavbar />
<Container>
<div className="flex items-center justify-center w-full h-full isolate">

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";
@ -795,7 +795,7 @@ export default function KvmIdRoute() {
</div>
</FocusTrap>
<div className="grid h-full grid-rows-(--grid-headerBody) select-none">
<div className="grid h-full select-none grid-rows-headerBody">
<DashboardNavbar
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
showConnectionStatus={true}
@ -809,7 +809,7 @@ export default function KvmIdRoute() {
<WebRTCVideo />
<div
style={{ animationDuration: "500ms" }}
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4 opacity-0"
>
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
{!!ConnectionStatusElement && ConnectionStatusElement}

View File

@ -8,7 +8,7 @@ export default function DevicesAlreadyAdopted() {
<>
<GridBackground />
<div className="grid min-h-screen grid-rows-(--grid-layout)">
<div className="grid min-h-screen grid-rows-layout">
<SimpleNavbar />
<Container>
<div className="flex items-center justify-center w-full h-full isolate">

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 EmptyCard from "@components/EmptyCard";
import KvmCard from "@components/KvmCard";
import { LinkButton } from "@components/Button";
import { User } from "@/hooks/stores";
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 { CLOUD_API } from "@/ui.config";
interface LoaderData {
@ -16,7 +16,7 @@ interface LoaderData {
user: User;
}
const loader = async () => {
export const loader = async () => {
const user = await checkAuth();
try {
@ -40,7 +40,7 @@ export default function DevicesRoute() {
useInterval(revalidate.revalidate, 4000);
return (
<div className="relative h-full">
<div className="grid h-full select-none grid-rows-(--grid-headerBody)">
<div className="grid h-full select-none grid-rows-headerBody">
<DashboardNavbar
isLoggedIn={!!user}
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
@ -101,5 +101,3 @@ export default function DevicesRoute() {
</div>
);
}
DevicesRoute.loader = loader;

View File

@ -56,7 +56,7 @@ export default function LoginLocalRoute() {
return (
<>
<GridBackground />
<div className="grid min-h-screen grid-rows-(--grid-layout)">
<div className="grid min-h-screen grid-rows-layout">
<SimpleNavbar />
<Container>
<div className="isolate flex h-full w-full items-center justify-center">

View File

@ -14,6 +14,7 @@ import api from "../api";
import { DeviceStatus } from "./welcome-local";
const loader = async () => {
const res = await api
.GET(`${DEVICE_API}/device/status`)
@ -58,24 +59,18 @@ export default function WelcomeLocalModeRoute() {
<GridBackground />
<div className="grid min-h-screen">
<Container>
<div className="isolate flex h-full w-full items-center justify-center">
<div className="flex items-center justify-center w-full h-full isolate">
<div className="max-w-xl space-y-8">
<div className="animate-fadeIn flex items-center justify-center opacity-0">
<img
src={LogoWhiteIcon}
alt=""
className="-ml-4 hidden h-[32px] dark:block"
/>
<div className="flex items-center justify-center opacity-0 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="animate-fadeIn space-y-2 text-center opacity-0"
className="space-y-2 text-center opacity-0 animate-fadeIn"
style={{ animationDelay: "200ms" }}
>
<h1 className="text-4xl font-semibold text-black dark:text-white">
Local Authentication Method
</h1>
<h1 className="text-4xl font-semibold text-black dark:text-white">Local Authentication Method</h1>
<p className="font-medium text-slate-600 dark:text-slate-400">
Select how you{"'"}d like to secure your JetKVM device locally.
</p>
@ -83,7 +78,7 @@ export default function WelcomeLocalModeRoute() {
<Form method="POST" className="space-y-8">
<div
className="animate-fadeIn grid grid-cols-1 gap-6 opacity-0 sm:grid-cols-2"
className="grid grid-cols-1 gap-6 opacity-0 animate-fadeIn sm:grid-cols-2"
style={{ animationDelay: "400ms" }}
>
{["password", "noPassword"].map(mode => (
@ -95,14 +90,14 @@ export default function WelcomeLocalModeRoute() {
})}
>
<div
className="relative flex cursor-pointer flex-col items-center p-6 select-none"
className="relative flex flex-col items-center p-6 cursor-pointer select-none"
onClick={() => setSelectedMode(mode as "password" | "noPassword")}
>
<div className="space-y-0 text-center">
<h3 className="text-base font-bold text-black dark:text-white">
{mode === "password" ? "Password protected" : "No Password"}
</h3>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
<p className="mt-2 text-sm text-center text-gray-600 dark:text-gray-400">
{mode === "password"
? "Secure your device with a password for added protection."
: "Quick access without password authentication."}
@ -116,7 +111,7 @@ export default function WelcomeLocalModeRoute() {
onChange={() => {
setSelectedMode(mode as "password" | "noPassword");
}}
className="absolute top-2 right-2 h-4 w-4 text-blue-600"
className="absolute w-4 h-4 text-blue-600 right-2 top-2"
/>
</div>
</GridCard>
@ -125,7 +120,7 @@ export default function WelcomeLocalModeRoute() {
{actionData?.error && (
<p
className="animate-fadeIn text-center text-sm text-red-600 opacity-0 dark:text-red-400"
className="text-sm text-center text-red-600 opacity-0 dark:text-red-400 animate-fadeIn"
style={{ animationDelay: "500ms" }}
>
{actionData.error}
@ -133,7 +128,7 @@ export default function WelcomeLocalModeRoute() {
)}
<div
className="animate-fadeIn mx-auto max-w-sm opacity-0"
className="max-w-sm mx-auto opacity-0 animate-fadeIn"
style={{ animationDelay: "500ms" }}
>
<Button
@ -149,7 +144,7 @@ export default function WelcomeLocalModeRoute() {
</Form>
<p
className="animate-fadeIn mx-auto max-w-md text-center text-xs text-slate-500 opacity-0 dark:text-slate-400"
className="max-w-md mx-auto text-xs text-center opacity-0 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

@ -69,34 +69,28 @@ export default function WelcomeLocalPasswordRoute() {
<GridBackground />
<div className="grid min-h-screen">
<Container>
<div className="isolate flex h-full w-full items-center justify-center">
<div className="flex items-center justify-center w-full h-full isolate">
<div className="max-w-2xl space-y-8">
<div className="animate-fadeIn flex items-center justify-center opacity-0">
<img
src={LogoWhiteIcon}
alt=""
className="-ml-4 hidden h-[32px] dark:block"
/>
<div className="flex items-center justify-center opacity-0 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="animate-fadeIn space-y-2 text-center opacity-0"
className="space-y-2 text-center opacity-0 animate-fadeIn"
style={{ animationDelay: "200ms" }}
>
<h1 className="text-4xl font-semibold text-black dark:text-white">
Set a Password
</h1>
<h1 className="text-4xl font-semibold text-black dark:text-white">Set a Password</h1>
<p className="font-medium text-slate-600 dark:text-slate-400">
Create a strong password to secure your JetKVM device locally.
</p>
</div>
<Fieldset className="space-y-12">
<Form method="POST" className="mx-auto max-w-sm space-y-4">
<Form method="POST" className="max-w-sm mx-auto space-y-4">
<div className="space-y-4">
<div
className="animate-fadeIn opacity-0"
className="opacity-0 animate-fadeIn"
style={{ animationDelay: "400ms" }}
>
<InputFieldWithLabel
@ -112,21 +106,21 @@ export default function WelcomeLocalPasswordRoute() {
onClick={() => setShowPassword(false)}
className="pointer-events-auto"
>
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
<LuEye className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>
) : (
<div
onClick={() => setShowPassword(true)}
className="pointer-events-auto"
>
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
<LuEyeOff className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>
)
}
/>
</div>
<div
className="animate-fadeIn opacity-0"
className="opacity-0 animate-fadeIn"
style={{ animationDelay: "400ms" }}
>
<InputFieldWithLabel
@ -143,7 +137,7 @@ export default function WelcomeLocalPasswordRoute() {
{actionData?.error && <p className="text-sm text-red-600">{}</p>}
<div
className="animate-fadeIn opacity-0"
className="opacity-0 animate-fadeIn"
style={{ animationDelay: "600ms" }}
>
<Button
@ -159,7 +153,7 @@ export default function WelcomeLocalPasswordRoute() {
</Fieldset>
<p
className="animate-fadeIn max-w-md text-center text-xs text-slate-500 opacity-0 dark:text-slate-400"
className="max-w-md text-xs text-center opacity-0 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

@ -13,6 +13,8 @@ import { DEVICE_API } from "@/ui.config";
import api from "../api";
export interface DeviceStatus {
isSetup: boolean;
}
@ -41,24 +43,19 @@ export default function WelcomeRoute() {
<div className="grid min-h-screen">
{imageLoaded && (
<Container>
<div className="isolate flex h-full w-full items-center justify-center">
<div className="flex items-center justify-center w-full h-full isolate">
<div className="max-w-3xl text-center">
<div className="space-y-8">
<div className="space-y-4">
<div className="animate-fadeIn animation-delay-1000 flex items-center justify-center opacity-0">
<img
src={LogoWhiteIcon}
alt="JetKVM Logo"
className="hidden h-[32px] dark:block"
/>
<img
src={LogoBlueIcon}
alt="JetKVM Logo"
className="h-[32px] dark:hidden"
/>
<div className="flex items-center justify-center opacity-0 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="animate-fadeIn animation-delay-1500 space-y-1 opacity-0">
<div
className="space-y-1 opacity-0 animate-fadeIn"
style={{ animationDelay: "1500ms" }}
>
<h1 className="text-4xl font-semibold text-black dark:text-white">
Welcome to JetKVM
</h1>
@ -72,19 +69,22 @@ export default function WelcomeRoute() {
<img
src={DeviceImage}
alt="JetKVM Device"
className="animation-delay-300 animate-fadeInScaleFloat max-w-md scale-[0.98] opacity-0 transition-all duration-1000 ease-out"
className="animation-delay-0 max-w-md scale-[0.98] animate-fadeInScaleFloat opacity-0 transition-all duration-1000 ease-out"
/>
</div>
</div>
<div className="-mt-8 space-y-4">
<p
style={{ animationDelay: "2000ms" }}
className="animate-fadeIn mx-auto max-w-lg text-lg text-slate-700 opacity-0 dark:text-slate-300"
className="max-w-lg mx-auto text-lg opacity-0 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 className="animate-fadeIn animation-delay-2300 opacity-0">
<div
style={{ animationDelay: "2300ms" }}
className="opacity-0 animate-fadeIn"
>
<LinkButton
size="LG"
theme="light"

View File

@ -5,9 +5,98 @@ import plugin from "tailwindcss/plugin";
/** @type {import("tailwindcss").Config} */
export default {
content: ["./src/**/*.{ts,tsx,svg}", "./index.html"],
darkMode: "selector",
theme: {
extend: {
gridTemplateRows: {
layout: "auto 1fr auto",
headerBody: "auto 1fr",
bodyFooter: "1fr auto",
},
gridTemplateColumns: {
sidebar: "1fr minmax(360px, 25%)",
},
screens: {
xs: "480px",
"2xl": "1440px",
"3xl": "1920px",
"4xl": "2560px",
},
fontFamily: {
sans: ["Circular", ...defaultTheme.fontFamily.sans],
display: ["Circular", ...defaultTheme.fontFamily.sans],
mono: ["Source Code Pro Variable", ...defaultTheme.fontFamily.mono],
},
maxWidth: {
"8xl": "88rem",
"9xl": "96rem",
"10xl": "104rem",
"11xl": "112rem",
"12xl": "120rem",
},
animation: {
enter: "enter .2s ease-out",
leave: "leave .15s ease-in forwards",
fadeInScale: "fadeInScale 1s ease-out forwards",
fadeInScaleFloat:
"fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite",
fadeIn: "fadeIn 1s ease-out forwards",
slideUpFade: "slideUpFade 1s ease-out forwards",
},
animationDelay: {
1000: "1000ms",
1500: "1500ms",
},
keyframes: {
enter: {
"0%": {
opacity: "0",
transform: "scale(.9)",
},
"100%": {
opacity: "1",
transform: "scale(1)",
},
},
leave: {
"0%": {
opacity: "1",
transform: "scale(1)",
},
"100%": {
opacity: "0",
transform: "scale(.9)",
},
},
fadeInScale: {
"0%": { opacity: "0", transform: "scale(0.98)" },
"100%": { opacity: "1", transform: "scale(1)" },
},
fadeInScaleFloat: {
"0%": { opacity: "0", transform: "scale(0.98) translateY(10px)" },
"100%": { opacity: "1", transform: "scale(1) translateY(0)" },
},
float: {
"0%, 100%": { transform: "translateY(0)" },
"50%": { transform: "translateY(-10px)" },
},
fadeIn: {
"0%": { opacity: "0", transform: "translateY(10px)" },
"70%": { opacity: "0.8", transform: "translateY(1px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
slideUpFade: {
"0%": { opacity: "0", transform: "translateY(20px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
},
},
},
plugins: [
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
require("@headlessui/tailwindcss"),
plugin(function ({ addVariant }) {
addVariant("disabled-within", `&:has(input:is(:disabled),button:is(:disabled))`);
}),
@ -53,5 +142,12 @@ export default {
},
);
},
function ({ addUtilities, theme }) {
const animationDelays = theme("animationDelay");
const utilities = Object.entries(animationDelays).map(([key, value]) => ({
[`.animation-delay-${key}`]: { animationDelay: value },
}));
addUtilities(utilities);
},
],
};

View File

@ -1,6 +1,5 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite";
import tsconfigPaths from "vite-tsconfig-paths";
import basicSsl from "@vitejs/plugin-basic-ssl";
@ -17,11 +16,7 @@ export default defineConfig(({ mode, command }) => {
const { JETKVM_PROXY_URL, USE_SSL } = process.env;
const useSSL = USE_SSL === "true";
const plugins = [
tailwindcss(),
tsconfigPaths(),
react()
];
const plugins = [tsconfigPaths(), react()];
if (useSSL) {
plugins.push(basicSsl());
}