mirror of https://github.com/jetkvm/kvm.git
Clean up Virtual Keyboard styling (#761)
* Clean up Virtual Keyboard styling Make the header a bit taller Add keyboard layout name to header Make the detached keyboard render smaller key text so you can read them. Updated the settings text for keyboard layout. * Add the key graphics and missing keys * style(ui): add cursor-pointer class to Button component for better UX * refactor(ui): Improve header styling and detach bug - Remove unused AttachIcon and related SVG asset. - Replace icon usage with a styled LinkButton to improve consistency. - Simplify and reformat VirtualKeyboard component for better readability. * refactor(ui): Hide keyboard layout settings on mobile and fix minor styling --------- Co-authored-by: Marc Brooks <IDisposable@gmail.com>
This commit is contained in:
parent
94521ef6db
commit
c68e15bf89
|
@ -1,8 +0,0 @@
|
|||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 6C2 4.89543 2.89543 4 4 4H20C21.1046 4 22 4.89543 22 6V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V6Z"
|
||||
fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M20 6H4V18H20V6ZM4 4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V6C22 4.89543 21.1046 4 20 4H4Z"
|
||||
fill="black"/>
|
||||
<path d="M4 13H20V18H4V13Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 511 B |
|
@ -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-hidden cursor-pointer",
|
||||
props.fullWidth ? "w-full" : "",
|
||||
loading ? "pointer-events-none" : "",
|
||||
);
|
||||
|
|
|
@ -2,33 +2,31 @@ import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Keyboard from "react-simple-keyboard";
|
||||
import { LuKeyboard } from "react-icons/lu";
|
||||
|
||||
import Card from "@components/Card";
|
||||
// eslint-disable-next-line import/order
|
||||
import { Button } from "@components/Button";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
|
||||
import "react-simple-keyboard/build/css/index.css";
|
||||
|
||||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||
import { cx } from "@/cva.config";
|
||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings";
|
||||
import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings";
|
||||
|
||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||
};
|
||||
|
||||
const AttachIcon = ({ className }: { className?: string }) => {
|
||||
return <img src={AttachIconRaw} alt="Attach Icon" className={className} />;
|
||||
};
|
||||
|
||||
function KeyboardWrapper() {
|
||||
const keyboardRef = useRef<HTMLDivElement>(null);
|
||||
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore();
|
||||
const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
||||
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } =
|
||||
useUiStore();
|
||||
const { keysDownState, isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } =
|
||||
useHidStore();
|
||||
const { handleKeyPress, executeMacro } = useKeyboard();
|
||||
const { selectedKeyboard } = useKeyboardLayout();
|
||||
|
||||
|
@ -44,29 +42,28 @@ function KeyboardWrapper() {
|
|||
return selectedKeyboard.virtualKeyboard;
|
||||
}, [selectedKeyboard]);
|
||||
|
||||
//const isCapsLockActive = useMemo(() => {
|
||||
// return (keyboardLedState.caps_lock);
|
||||
//}, [keyboardLedState]);
|
||||
|
||||
const { isShiftActive, /*isControlActive, isAltActive, isMetaActive, isAltGrActive*/ } = useMemo(() => {
|
||||
const { isShiftActive } = useMemo(() => {
|
||||
return decodeModifiers(keysDownState.modifier);
|
||||
}, [keysDownState]);
|
||||
|
||||
const mainLayoutName = useMemo(() => {
|
||||
const layoutName = isShiftActive ? "shift": "default";
|
||||
return layoutName;
|
||||
return isShiftActive ? "shift" : "default";
|
||||
}, [isShiftActive]);
|
||||
|
||||
const keyNamesForDownKeys = useMemo(() => {
|
||||
const activeModifierMask = keysDownState.modifier || 0;
|
||||
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
||||
const modifierNames = Object.entries(modifiers)
|
||||
.filter(([_, mask]) => (activeModifierMask & mask) !== 0)
|
||||
.map(([name, _]) => name);
|
||||
|
||||
const keysDown = keysDownState.keys || [];
|
||||
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
||||
const keyNames = Object.entries(keys)
|
||||
.filter(([_, value]) => keysDown.includes(value))
|
||||
.map(([name, _]) => name);
|
||||
|
||||
return [...modifierNames,...keyNames, ' ']; // we have to have at least one space to avoid keyboard whining
|
||||
return [...modifierNames, ...keyNames, " "]; // we have to have at least one space to avoid keyboard whining
|
||||
}, [keysDownState]);
|
||||
|
||||
|
||||
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
if (!keyboardRef.current) return;
|
||||
if (e instanceof TouchEvent && e.touches.length > 1) return;
|
||||
|
@ -110,6 +107,9 @@ function KeyboardWrapper() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Is the keyboard detached or attached?
|
||||
if (isAttachedVirtualKeyboardVisible) return;
|
||||
|
||||
const handle = keyboardRef.current;
|
||||
if (handle) {
|
||||
handle.addEventListener("touchstart", startDrag);
|
||||
|
@ -134,15 +134,12 @@ function KeyboardWrapper() {
|
|||
document.removeEventListener("mousemove", onDrag);
|
||||
document.removeEventListener("touchmove", onDrag);
|
||||
};
|
||||
}, [endDrag, onDrag, startDrag]);
|
||||
}, [isAttachedVirtualKeyboardVisible, endDrag, onDrag, startDrag]);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
async (_: string, e: MouseEvent | undefined) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
},
|
||||
[]
|
||||
);
|
||||
const onKeyUp = useCallback(async (_: string, e: MouseEvent | undefined) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
async (key: string, e: MouseEvent | undefined) => {
|
||||
|
@ -151,24 +148,30 @@ function KeyboardWrapper() {
|
|||
|
||||
// handle the fake key-macros we have defined for common combinations
|
||||
if (key === "CtrlAltDelete") {
|
||||
await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||
await executeMacro([
|
||||
{ keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "AltMetaEscape") {
|
||||
await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]);
|
||||
await executeMacro([
|
||||
{ keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "CtrlAltBackspace") {
|
||||
await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||
await executeMacro([
|
||||
{ keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer)
|
||||
if (latchingKeys.includes(key)) {
|
||||
console.debug(`Latching key pressed: ${key} sending down and delayed up pair`);
|
||||
handleKeyPress(keys[key], true)
|
||||
handleKeyPress(keys[key], true);
|
||||
setTimeout(() => handleKeyPress(keys[key], false), 100);
|
||||
return;
|
||||
}
|
||||
|
@ -176,8 +179,10 @@ function KeyboardWrapper() {
|
|||
// if they press any of the dynamic keys, we send a keypress down event but we don't release it until they click it again
|
||||
if (Object.keys(modifiers).includes(key)) {
|
||||
const currentlyDown = keyNamesForDownKeys.includes(key);
|
||||
console.debug(`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`);
|
||||
handleKeyPress(keys[key], !currentlyDown)
|
||||
console.debug(
|
||||
`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`,
|
||||
);
|
||||
handleKeyPress(keys[key], !currentlyDown);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -211,7 +216,7 @@ function KeyboardWrapper() {
|
|||
<div
|
||||
className={cx(
|
||||
!isAttachedVirtualKeyboardVisible
|
||||
? "fixed left-0 top-0 z-50 select-none"
|
||||
? "fixed top-0 left-0 z-10 select-none"
|
||||
: "relative",
|
||||
)}
|
||||
ref={keyboardRef}
|
||||
|
@ -224,9 +229,10 @@ function KeyboardWrapper() {
|
|||
<Card
|
||||
className={cx("overflow-hidden", {
|
||||
"rounded-none": isAttachedVirtualKeyboardVisible,
|
||||
"keyboard-detached": !isAttachedVirtualKeyboardVisible,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-center border-b border-b-slate-800/30 bg-white px-2 py-1 dark:border-b-slate-300/20 dark:bg-slate-800">
|
||||
<div className="flex items-center justify-center border-b border-b-slate-800/30 bg-white px-2 py-4 dark:border-b-slate-300/20 dark:bg-slate-800">
|
||||
<div className="absolute left-2 flex items-center gap-x-2">
|
||||
{isAttachedVirtualKeyboardVisible ? (
|
||||
<Button
|
||||
|
@ -240,15 +246,25 @@ function KeyboardWrapper() {
|
|||
size="XS"
|
||||
theme="light"
|
||||
text="Attach"
|
||||
LeadingIcon={AttachIcon}
|
||||
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="select-none self-center font-sans text-[12px] text-slate-700 dark:text-slate-300">
|
||||
<h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300">
|
||||
Virtual Keyboard
|
||||
</h2>
|
||||
<div className="absolute right-2">
|
||||
<div className="absolute right-2 flex items-center gap-x-2">
|
||||
<div className="hidden md:flex gap-x-2 items-center">
|
||||
<LinkButton
|
||||
size="XS"
|
||||
to="settings/keyboard"
|
||||
theme="light"
|
||||
text={selectedKeyboard.name}
|
||||
LeadingIcon={LuKeyboard}
|
||||
/>
|
||||
<div className="h-[20px] w-px bg-slate-800/20 dark:bg-slate-200/20" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
|
@ -317,7 +333,7 @@ function KeyboardWrapper() {
|
|||
stopMouseUpPropagation={true}
|
||||
/>
|
||||
</div>
|
||||
{ /* TODO add optional number pad */ }
|
||||
{/* TODO add optional number pad */}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -325,6 +325,20 @@ video::-webkit-media-controls {
|
|||
@apply mr-[2px]! md:mr-[5px]!;
|
||||
}
|
||||
|
||||
/* Reduce font size for selected keys when keyboard is detached */
|
||||
.keyboard-detached .simple-keyboard-main.simple-keyboard {
|
||||
min-width: calc(14 * 7ch);
|
||||
}
|
||||
|
||||
.keyboard-detached .simple-keyboard.hg-theme-default div.hg-button {
|
||||
text-wrap: auto;
|
||||
text-align: center;
|
||||
min-width: 6ch;
|
||||
}
|
||||
.keyboard-detached .simple-keyboard.hg-theme-default .hg-button span {
|
||||
font-size: 50%;
|
||||
}
|
||||
|
||||
/* Hide the scrollbar by setting the scrollbar color to the background color */
|
||||
.xterm .xterm-viewport {
|
||||
scrollbar-color: var(--color-gray-900) #002b36;
|
||||
|
|
|
@ -144,33 +144,33 @@ export const keyDisplayMap: Record<string, string> = {
|
|||
AltMetaEscape: "Alt + Meta + Escape",
|
||||
CtrlAltBackspace: "Ctrl + Alt + Backspace",
|
||||
AltGr: "AltGr",
|
||||
AltLeft: "Alt",
|
||||
AltRight: "Alt",
|
||||
AltLeft: "Alt ⌥",
|
||||
AltRight: "⌥ Alt",
|
||||
ArrowDown: "↓",
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
ArrowUp: "↑",
|
||||
Backspace: "Backspace",
|
||||
"(Backspace)": "Backspace",
|
||||
CapsLock: "Caps Lock",
|
||||
CapsLock: "Caps Lock ⇪",
|
||||
Clear: "Clear",
|
||||
ControlLeft: "Ctrl",
|
||||
ControlRight: "Ctrl",
|
||||
Delete: "Delete",
|
||||
ControlLeft: "Ctrl ⌃",
|
||||
ControlRight: "⌃ Ctrl",
|
||||
Delete: "Delete ⌦",
|
||||
End: "End",
|
||||
Enter: "Enter",
|
||||
Escape: "Esc",
|
||||
Home: "Home",
|
||||
Insert: "Insert",
|
||||
Menu: "Menu",
|
||||
MetaLeft: "Meta",
|
||||
MetaRight: "Meta",
|
||||
MetaLeft: "Meta ⌘",
|
||||
MetaRight: "⌘ Meta",
|
||||
PageDown: "PgDn",
|
||||
PageUp: "PgUp",
|
||||
ShiftLeft: "Shift",
|
||||
ShiftRight: "Shift",
|
||||
ShiftLeft: "Shift ⇧",
|
||||
ShiftRight: "⇧ Shift",
|
||||
Space: " ",
|
||||
Tab: "Tab",
|
||||
Tab: "Tab ⇥",
|
||||
|
||||
// Letters
|
||||
KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e",
|
||||
|
|
|
@ -81,12 +81,6 @@ export const keys = {
|
|||
Help: 0x75,
|
||||
Home: 0x4a,
|
||||
Insert: 0x49,
|
||||
International1: 0x87,
|
||||
International2: 0x88,
|
||||
International3: 0x89,
|
||||
International4: 0x8a,
|
||||
International5: 0x8b,
|
||||
International6: 0x8c,
|
||||
International7: 0x8d,
|
||||
International8: 0x8e,
|
||||
International9: 0x8f,
|
||||
|
@ -117,14 +111,20 @@ export const keys = {
|
|||
KeyX: 0x1b,
|
||||
KeyY: 0x1c,
|
||||
KeyZ: 0x1d,
|
||||
KeyRO: 0x87,
|
||||
KatakanaHiragana: 0x88,
|
||||
Yen: 0x89,
|
||||
Henkan: 0x8a,
|
||||
Muhenkan: 0x8b,
|
||||
KPJPComma: 0x8c,
|
||||
Hangeul: 0x90,
|
||||
Hanja: 0x91,
|
||||
Katakana: 0x92,
|
||||
Hiragana: 0x93,
|
||||
ZenkakuHankaku:0x94,
|
||||
LockingCapsLock: 0x82,
|
||||
LockingNumLock: 0x83,
|
||||
LockingScrollLock: 0x84,
|
||||
Lang1: 0x90, // Hangul/English toggle on Korean keyboards
|
||||
Lang2: 0x91, // Hanja conversion on Korean keyboards
|
||||
Lang3: 0x92, // Katakana on Japanese keyboards
|
||||
Lang4: 0x93, // Hiragana on Japanese keyboards
|
||||
Lang5: 0x94, // Zenkaku/Hankaku toggle on Japanese keyboards
|
||||
Lang6: 0x95,
|
||||
Lang7: 0x96,
|
||||
Lang8: 0x97,
|
||||
|
@ -157,7 +157,7 @@ export const keys = {
|
|||
NumpadClearEntry: 0xd9,
|
||||
NumpadColon: 0xcb,
|
||||
NumpadComma: 0x85,
|
||||
NumpadDecimal: 0x63,
|
||||
NumpadDecimal: 0x63, // and Delete
|
||||
NumpadDecimalBase: 0xdc,
|
||||
NumpadDelete: 0x63,
|
||||
NumpadDivide: 0x54,
|
||||
|
@ -211,7 +211,7 @@ export const keys = {
|
|||
PageUp: 0x4b,
|
||||
Paste: 0x7d,
|
||||
Pause: 0x48,
|
||||
Period: 0x37,
|
||||
Period: 0x37, // aka Dot
|
||||
Power: 0x66,
|
||||
PrintScreen: 0x46,
|
||||
Prior: 0x9d,
|
||||
|
@ -226,7 +226,7 @@ export const keys = {
|
|||
Slash: 0x38,
|
||||
Space: 0x2c,
|
||||
Stop: 0x78,
|
||||
SystemRequest: 0x9a,
|
||||
SystemRequest: 0x9a, // aka Attention
|
||||
Tab: 0x2b,
|
||||
ThousandsSeparator: 0xb2,
|
||||
Tilde: 0x35,
|
||||
|
|
|
@ -53,7 +53,7 @@ export default function SettingsKeyboardRoute() {
|
|||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Paste text"
|
||||
title="Keyboard Layout"
|
||||
description="Keyboard layout of target operating system"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
|
@ -66,7 +66,7 @@ export default function SettingsKeyboardRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
||||
The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Reference in New Issue