Skip to content

Commit c68e15b

Browse files
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 <[email protected]>
1 parent 94521ef commit c68e15b

File tree

7 files changed

+98
-76
lines changed

7 files changed

+98
-76
lines changed

ui/src/assets/attach-icon.svg

Lines changed: 0 additions & 8 deletions
This file was deleted.

ui/src/components/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ type ButtonPropsType = Pick<
175175
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
176176
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
177177
const classes = cx(
178-
"group outline-hidden",
178+
"group outline-hidden cursor-pointer",
179179
props.fullWidth ? "w-full" : "",
180180
loading ? "pointer-events-none" : "",
181181
);

ui/src/components/VirtualKeyboard.tsx

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,31 @@ import { ChevronDownIcon } from "@heroicons/react/16/solid";
22
import { AnimatePresence, motion } from "framer-motion";
33
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import Keyboard from "react-simple-keyboard";
5+
import { LuKeyboard } from "react-icons/lu";
56

67
import Card from "@components/Card";
78
// eslint-disable-next-line import/order
8-
import { Button } from "@components/Button";
9+
import { Button, LinkButton } from "@components/Button";
910

1011
import "react-simple-keyboard/build/css/index.css";
1112

12-
import AttachIconRaw from "@/assets/attach-icon.svg";
1313
import DetachIconRaw from "@/assets/detach-icon.svg";
1414
import { cx } from "@/cva.config";
1515
import { useHidStore, useUiStore } from "@/hooks/stores";
1616
import useKeyboard from "@/hooks/useKeyboard";
1717
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
18-
import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings";
18+
import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings";
1919

2020
export const DetachIcon = ({ className }: { className?: string }) => {
2121
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
2222
};
2323

24-
const AttachIcon = ({ className }: { className?: string }) => {
25-
return <img src={AttachIconRaw} alt="Attach Icon" className={className} />;
26-
};
27-
2824
function KeyboardWrapper() {
2925
const keyboardRef = useRef<HTMLDivElement>(null);
30-
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore();
31-
const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
26+
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } =
27+
useUiStore();
28+
const { keysDownState, isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } =
29+
useHidStore();
3230
const { handleKeyPress, executeMacro } = useKeyboard();
3331
const { selectedKeyboard } = useKeyboardLayout();
3432

@@ -44,29 +42,28 @@ function KeyboardWrapper() {
4442
return selectedKeyboard.virtualKeyboard;
4543
}, [selectedKeyboard]);
4644

47-
//const isCapsLockActive = useMemo(() => {
48-
// return (keyboardLedState.caps_lock);
49-
//}, [keyboardLedState]);
50-
51-
const { isShiftActive, /*isControlActive, isAltActive, isMetaActive, isAltGrActive*/ } = useMemo(() => {
45+
const { isShiftActive } = useMemo(() => {
5246
return decodeModifiers(keysDownState.modifier);
5347
}, [keysDownState]);
5448

5549
const mainLayoutName = useMemo(() => {
56-
const layoutName = isShiftActive ? "shift": "default";
57-
return layoutName;
50+
return isShiftActive ? "shift" : "default";
5851
}, [isShiftActive]);
5952

6053
const keyNamesForDownKeys = useMemo(() => {
6154
const activeModifierMask = keysDownState.modifier || 0;
62-
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
55+
const modifierNames = Object.entries(modifiers)
56+
.filter(([_, mask]) => (activeModifierMask & mask) !== 0)
57+
.map(([name, _]) => name);
6358

6459
const keysDown = keysDownState.keys || [];
65-
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
60+
const keyNames = Object.entries(keys)
61+
.filter(([_, value]) => keysDown.includes(value))
62+
.map(([name, _]) => name);
6663

67-
return [...modifierNames,...keyNames, ' ']; // we have to have at least one space to avoid keyboard whining
64+
return [...modifierNames, ...keyNames, " "]; // we have to have at least one space to avoid keyboard whining
6865
}, [keysDownState]);
69-
66+
7067
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
7168
if (!keyboardRef.current) return;
7269
if (e instanceof TouchEvent && e.touches.length > 1) return;
@@ -110,6 +107,9 @@ function KeyboardWrapper() {
110107
}, []);
111108

112109
useEffect(() => {
110+
// Is the keyboard detached or attached?
111+
if (isAttachedVirtualKeyboardVisible) return;
112+
113113
const handle = keyboardRef.current;
114114
if (handle) {
115115
handle.addEventListener("touchstart", startDrag);
@@ -134,15 +134,12 @@ function KeyboardWrapper() {
134134
document.removeEventListener("mousemove", onDrag);
135135
document.removeEventListener("touchmove", onDrag);
136136
};
137-
}, [endDrag, onDrag, startDrag]);
137+
}, [isAttachedVirtualKeyboardVisible, endDrag, onDrag, startDrag]);
138138

139-
const onKeyUp = useCallback(
140-
async (_: string, e: MouseEvent | undefined) => {
141-
e?.preventDefault();
142-
e?.stopPropagation();
143-
},
144-
[]
145-
);
139+
const onKeyUp = useCallback(async (_: string, e: MouseEvent | undefined) => {
140+
e?.preventDefault();
141+
e?.stopPropagation();
142+
}, []);
146143

147144
const onKeyDown = useCallback(
148145
async (key: string, e: MouseEvent | undefined) => {
@@ -151,33 +148,41 @@ function KeyboardWrapper() {
151148

152149
// handle the fake key-macros we have defined for common combinations
153150
if (key === "CtrlAltDelete") {
154-
await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
151+
await executeMacro([
152+
{ keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 },
153+
]);
155154
return;
156155
}
157156

158157
if (key === "AltMetaEscape") {
159-
await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]);
158+
await executeMacro([
159+
{ keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 },
160+
]);
160161
return;
161162
}
162163

163164
if (key === "CtrlAltBackspace") {
164-
await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
165+
await executeMacro([
166+
{ keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 },
167+
]);
165168
return;
166169
}
167170

168171
// if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer)
169172
if (latchingKeys.includes(key)) {
170173
console.debug(`Latching key pressed: ${key} sending down and delayed up pair`);
171-
handleKeyPress(keys[key], true)
174+
handleKeyPress(keys[key], true);
172175
setTimeout(() => handleKeyPress(keys[key], false), 100);
173176
return;
174177
}
175178

176179
// 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
177180
if (Object.keys(modifiers).includes(key)) {
178181
const currentlyDown = keyNamesForDownKeys.includes(key);
179-
console.debug(`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`);
180-
handleKeyPress(keys[key], !currentlyDown)
182+
console.debug(
183+
`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`,
184+
);
185+
handleKeyPress(keys[key], !currentlyDown);
181186
return;
182187
}
183188

@@ -211,7 +216,7 @@ function KeyboardWrapper() {
211216
<div
212217
className={cx(
213218
!isAttachedVirtualKeyboardVisible
214-
? "fixed left-0 top-0 z-50 select-none"
219+
? "fixed top-0 left-0 z-10 select-none"
215220
: "relative",
216221
)}
217222
ref={keyboardRef}
@@ -224,9 +229,10 @@ function KeyboardWrapper() {
224229
<Card
225230
className={cx("overflow-hidden", {
226231
"rounded-none": isAttachedVirtualKeyboardVisible,
232+
"keyboard-detached": !isAttachedVirtualKeyboardVisible,
227233
})}
228234
>
229-
<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">
235+
<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">
230236
<div className="absolute left-2 flex items-center gap-x-2">
231237
{isAttachedVirtualKeyboardVisible ? (
232238
<Button
@@ -240,15 +246,25 @@ function KeyboardWrapper() {
240246
size="XS"
241247
theme="light"
242248
text="Attach"
243-
LeadingIcon={AttachIcon}
244249
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
245250
/>
246251
)}
247252
</div>
248-
<h2 className="select-none self-center font-sans text-[12px] text-slate-700 dark:text-slate-300">
253+
<h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300">
249254
Virtual Keyboard
250255
</h2>
251-
<div className="absolute right-2">
256+
<div className="absolute right-2 flex items-center gap-x-2">
257+
<div className="hidden md:flex gap-x-2 items-center">
258+
<LinkButton
259+
size="XS"
260+
to="settings/keyboard"
261+
theme="light"
262+
text={selectedKeyboard.name}
263+
LeadingIcon={LuKeyboard}
264+
/>
265+
<div className="h-[20px] w-px bg-slate-800/20 dark:bg-slate-200/20" />
266+
</div>
267+
252268
<Button
253269
size="XS"
254270
theme="light"
@@ -317,7 +333,7 @@ function KeyboardWrapper() {
317333
stopMouseUpPropagation={true}
318334
/>
319335
</div>
320-
{ /* TODO add optional number pad */ }
336+
{/* TODO add optional number pad */}
321337
</div>
322338
</div>
323339
</Card>

ui/src/index.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,20 @@ video::-webkit-media-controls {
325325
@apply mr-[2px]! md:mr-[5px]!;
326326
}
327327

328+
/* Reduce font size for selected keys when keyboard is detached */
329+
.keyboard-detached .simple-keyboard-main.simple-keyboard {
330+
min-width: calc(14 * 7ch);
331+
}
332+
333+
.keyboard-detached .simple-keyboard.hg-theme-default div.hg-button {
334+
text-wrap: auto;
335+
text-align: center;
336+
min-width: 6ch;
337+
}
338+
.keyboard-detached .simple-keyboard.hg-theme-default .hg-button span {
339+
font-size: 50%;
340+
}
341+
328342
/* Hide the scrollbar by setting the scrollbar color to the background color */
329343
.xterm .xterm-viewport {
330344
scrollbar-color: var(--color-gray-900) #002b36;

ui/src/keyboardLayouts/en_US.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,33 +144,33 @@ export const keyDisplayMap: Record<string, string> = {
144144
AltMetaEscape: "Alt + Meta + Escape",
145145
CtrlAltBackspace: "Ctrl + Alt + Backspace",
146146
AltGr: "AltGr",
147-
AltLeft: "Alt",
148-
AltRight: "Alt",
147+
AltLeft: "Alt",
148+
AltRight: "Alt",
149149
ArrowDown: "↓",
150150
ArrowLeft: "←",
151151
ArrowRight: "→",
152152
ArrowUp: "↑",
153153
Backspace: "Backspace",
154154
"(Backspace)": "Backspace",
155-
CapsLock: "Caps Lock",
155+
CapsLock: "Caps Lock",
156156
Clear: "Clear",
157-
ControlLeft: "Ctrl",
158-
ControlRight: "Ctrl",
159-
Delete: "Delete",
157+
ControlLeft: "Ctrl",
158+
ControlRight: "Ctrl",
159+
Delete: "Delete",
160160
End: "End",
161161
Enter: "Enter",
162162
Escape: "Esc",
163163
Home: "Home",
164164
Insert: "Insert",
165165
Menu: "Menu",
166-
MetaLeft: "Meta",
167-
MetaRight: "Meta",
166+
MetaLeft: "Meta",
167+
MetaRight: "Meta",
168168
PageDown: "PgDn",
169169
PageUp: "PgUp",
170-
ShiftLeft: "Shift",
171-
ShiftRight: "Shift",
170+
ShiftLeft: "Shift",
171+
ShiftRight: "Shift",
172172
Space: " ",
173-
Tab: "Tab",
173+
Tab: "Tab",
174174

175175
// Letters
176176
KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e",

ui/src/keyboardMappings.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,6 @@ export const keys = {
8181
Help: 0x75,
8282
Home: 0x4a,
8383
Insert: 0x49,
84-
International1: 0x87,
85-
International2: 0x88,
86-
International3: 0x89,
87-
International4: 0x8a,
88-
International5: 0x8b,
89-
International6: 0x8c,
9084
International7: 0x8d,
9185
International8: 0x8e,
9286
International9: 0x8f,
@@ -117,14 +111,20 @@ export const keys = {
117111
KeyX: 0x1b,
118112
KeyY: 0x1c,
119113
KeyZ: 0x1d,
114+
KeyRO: 0x87,
115+
KatakanaHiragana: 0x88,
116+
Yen: 0x89,
117+
Henkan: 0x8a,
118+
Muhenkan: 0x8b,
119+
KPJPComma: 0x8c,
120+
Hangeul: 0x90,
121+
Hanja: 0x91,
122+
Katakana: 0x92,
123+
Hiragana: 0x93,
124+
ZenkakuHankaku:0x94,
120125
LockingCapsLock: 0x82,
121126
LockingNumLock: 0x83,
122127
LockingScrollLock: 0x84,
123-
Lang1: 0x90, // Hangul/English toggle on Korean keyboards
124-
Lang2: 0x91, // Hanja conversion on Korean keyboards
125-
Lang3: 0x92, // Katakana on Japanese keyboards
126-
Lang4: 0x93, // Hiragana on Japanese keyboards
127-
Lang5: 0x94, // Zenkaku/Hankaku toggle on Japanese keyboards
128128
Lang6: 0x95,
129129
Lang7: 0x96,
130130
Lang8: 0x97,
@@ -157,7 +157,7 @@ export const keys = {
157157
NumpadClearEntry: 0xd9,
158158
NumpadColon: 0xcb,
159159
NumpadComma: 0x85,
160-
NumpadDecimal: 0x63,
160+
NumpadDecimal: 0x63, // and Delete
161161
NumpadDecimalBase: 0xdc,
162162
NumpadDelete: 0x63,
163163
NumpadDivide: 0x54,
@@ -211,7 +211,7 @@ export const keys = {
211211
PageUp: 0x4b,
212212
Paste: 0x7d,
213213
Pause: 0x48,
214-
Period: 0x37,
214+
Period: 0x37, // aka Dot
215215
Power: 0x66,
216216
PrintScreen: 0x46,
217217
Prior: 0x9d,
@@ -226,7 +226,7 @@ export const keys = {
226226
Slash: 0x38,
227227
Space: 0x2c,
228228
Stop: 0x78,
229-
SystemRequest: 0x9a,
229+
SystemRequest: 0x9a, // aka Attention
230230
Tab: 0x2b,
231231
ThousandsSeparator: 0xb2,
232232
Tilde: 0x35,

ui/src/routes/devices.$id.settings.keyboard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default function SettingsKeyboardRoute() {
5353

5454
<div className="space-y-4">
5555
<SettingsItem
56-
title="Paste text"
56+
title="Keyboard Layout"
5757
description="Keyboard layout of target operating system"
5858
>
5959
<SelectMenuBasic
@@ -66,7 +66,7 @@ export default function SettingsKeyboardRoute() {
6666
/>
6767
</SettingsItem>
6868
<p className="text-xs text-slate-600 dark:text-slate-400">
69-
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.
69+
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.
7070
</p>
7171
</div>
7272

0 commit comments

Comments
 (0)