From c68e15bf8968caa170701f39d2c2a30737edcacc Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 3 Sep 2025 11:33:07 +0200 Subject: [PATCH] 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 --- ui/src/assets/attach-icon.svg | 8 -- ui/src/components/Button.tsx | 2 +- ui/src/components/VirtualKeyboard.tsx | 96 +++++++++++-------- ui/src/index.css | 14 +++ ui/src/keyboardLayouts/en_US.ts | 22 ++--- ui/src/keyboardMappings.ts | 28 +++--- .../routes/devices.$id.settings.keyboard.tsx | 4 +- 7 files changed, 98 insertions(+), 76 deletions(-) delete mode 100644 ui/src/assets/attach-icon.svg diff --git a/ui/src/assets/attach-icon.svg b/ui/src/assets/attach-icon.svg deleted file mode 100644 index 88deb80..0000000 --- a/ui/src/assets/attach-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 97fcc5f..b7f0950 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -175,7 +175,7 @@ type ButtonPropsType = Pick< export const Button = React.forwardRef( ({ 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" : "", ); diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index ce1bd83..60bfa7b 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -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 Detach Icon; }; -const AttachIcon = ({ className }: { className?: string }) => { - return Attach Icon; -}; - function KeyboardWrapper() { const keyboardRef = useRef(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() {
-
+
{isAttachedVirtualKeyboardVisible ? (
-

+

Virtual Keyboard

-
+
+
+ +
+
+
- { /* TODO add optional number pad */ } + {/* TODO add optional number pad */}
diff --git a/ui/src/index.css b/ui/src/index.css index db03b42..ae23db2 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -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; diff --git a/ui/src/keyboardLayouts/en_US.ts b/ui/src/keyboardLayouts/en_US.ts index 872d356..2076ce6 100644 --- a/ui/src/keyboardLayouts/en_US.ts +++ b/ui/src/keyboardLayouts/en_US.ts @@ -144,33 +144,33 @@ export const keyDisplayMap: Record = { 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", diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 14b0c60..1ffc8d7 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -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, diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index abd72bf..6f5c2e8 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -53,7 +53,7 @@ export default function SettingsKeyboardRoute() {

- 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.