Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev
This commit is contained in:
@@ -39,7 +39,7 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onKeepFocus,
|
onKeepFocus,
|
||||||
}) => {
|
}) => {
|
||||||
const { sendKey } = useSpecialKeys({ onSend });
|
const { sendKey, clearModifier } = useSpecialKeys({ onSend });
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
@@ -47,14 +47,29 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendKey(key);
|
sendKey(key);
|
||||||
onClose();
|
onClose();
|
||||||
onKeepFocus?.();
|
// Always refocus terminal after sending
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
onKeepFocus?.();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOverlayPointerDown = (e: React.PointerEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearModifier();
|
||||||
|
onClose();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
onKeepFocus?.();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="special-keys-panel-overlay" onClick={onClose}>
|
<div
|
||||||
|
className="special-keys-panel-overlay"
|
||||||
|
onPointerDown={handleOverlayPointerDown}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="special-keys-panel"
|
className="special-keys-panel"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="special-keys-panel-section">
|
<div className="special-keys-panel-section">
|
||||||
{EXPANDED_KEYS.map(({ key, label }) => (
|
{EXPANDED_KEYS.map(({ key, label }) => (
|
||||||
@@ -62,7 +77,6 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
|
|||||||
key={key}
|
key={key}
|
||||||
className="special-key-button"
|
className="special-key-button"
|
||||||
onPointerDown={(e) => handlePointerDown(e, key)}
|
onPointerDown={(e) => handlePointerDown(e, key)}
|
||||||
onFocus={(e) => e.target.blur()}
|
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
@@ -77,7 +91,6 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
|
|||||||
key={key}
|
key={key}
|
||||||
className="special-key-button"
|
className="special-key-button"
|
||||||
onPointerDown={(e) => handlePointerDown(e, key)}
|
onPointerDown={(e) => handlePointerDown(e, key)}
|
||||||
onFocus={(e) => e.target.blur()}
|
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSpecialKeys, type SpecialKey } from "../hooks/use-special-keys";
|
import { useSpecialKeys, type SpecialKey, type ModifierKey } from "../hooks/use-special-keys";
|
||||||
|
|
||||||
interface SpecialKeysStripProps {
|
interface SpecialKeysStripProps {
|
||||||
onSend: (data: string) => void;
|
onSend: (data: string) => void;
|
||||||
@@ -8,11 +8,11 @@ interface SpecialKeysStripProps {
|
|||||||
onKeepFocus?: () => void;
|
onKeepFocus?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRIMARY_KEYS: { key: SpecialKey; label: string }[] = [
|
const PRIMARY_KEYS: { key: SpecialKey; label: string; isModifier?: boolean }[] = [
|
||||||
{ key: "escape", label: "Esc" },
|
{ key: "escape", label: "Esc" },
|
||||||
{ key: "tab", label: "Tab" },
|
{ key: "tab", label: "Tab" },
|
||||||
{ key: "ctrl", label: "Ctrl" },
|
{ key: "ctrl", label: "Ctrl", isModifier: true },
|
||||||
{ key: "alt", label: "Alt" },
|
{ key: "alt", label: "Alt", isModifier: true },
|
||||||
{ key: "up", label: "↑" },
|
{ key: "up", label: "↑" },
|
||||||
{ key: "down", label: "↓" },
|
{ key: "down", label: "↓" },
|
||||||
{ key: "left", label: "←" },
|
{ key: "left", label: "←" },
|
||||||
@@ -25,31 +25,38 @@ export const SpecialKeysStrip: React.FC<SpecialKeysStripProps> = ({
|
|||||||
onMoreClick,
|
onMoreClick,
|
||||||
onKeepFocus,
|
onKeepFocus,
|
||||||
}) => {
|
}) => {
|
||||||
const { sendKey } = useSpecialKeys({ onSend });
|
const { sendKey, activeModifier } = useSpecialKeys({ onSend });
|
||||||
|
|
||||||
const handlePointerDown = (e: React.PointerEvent, key: SpecialKey) => {
|
const handlePointerDown = (e: React.PointerEvent, key: SpecialKey) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendKey(key);
|
sendKey(key);
|
||||||
onKeepFocus?.();
|
// Always refocus terminal after sending
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
onKeepFocus?.();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMorePointerDown = (e: React.PointerEvent) => {
|
const handleMorePointerDown = (e: React.PointerEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onMoreClick?.();
|
onMoreClick?.();
|
||||||
onKeepFocus?.();
|
requestAnimationFrame(() => {
|
||||||
|
onKeepFocus?.();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`special-keys-strip ${isVisible ? "visible" : "hidden"}`}>
|
<div className={`special-keys-strip ${isVisible ? "visible" : "hidden"}`}>
|
||||||
{PRIMARY_KEYS.map(({ key, label }) => (
|
{PRIMARY_KEYS.map(({ key, label, isModifier }) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
className="special-key-button"
|
className={`special-key-button ${
|
||||||
|
isModifier && activeModifier === key ? "active-modifier" : ""
|
||||||
|
}`}
|
||||||
onPointerDown={(e) => handlePointerDown(e, key)}
|
onPointerDown={(e) => handlePointerDown(e, key)}
|
||||||
onFocus={(e) => e.target.blur()}
|
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label={`Send ${label}`}
|
aria-label={`Send ${label}`}
|
||||||
|
aria-pressed={isModifier && activeModifier === key}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
@@ -58,7 +65,6 @@ export const SpecialKeysStrip: React.FC<SpecialKeysStripProps> = ({
|
|||||||
<button
|
<button
|
||||||
className="special-key-button special-key-more"
|
className="special-key-button special-key-more"
|
||||||
onPointerDown={handleMorePointerDown}
|
onPointerDown={handleMorePointerDown}
|
||||||
onFocus={(e) => e.target.blur()}
|
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label="More special keys"
|
aria-label="More special keys"
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
hiddenInputRef.current?.focus();
|
termRef.current?.focus();
|
||||||
};
|
};
|
||||||
onTerminalReadyRef.current(sendData, status, focusInput);
|
onTerminalReadyRef.current(sendData, status, focusInput);
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
hiddenInputRef.current?.focus();
|
termRef.current?.focus();
|
||||||
};
|
};
|
||||||
onTerminalReady(sendData, status, focusInput);
|
onTerminalReady(sendData, status, focusInput);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
|
|
||||||
export type SpecialKey =
|
export type SpecialKey =
|
||||||
| "escape"
|
| "escape"
|
||||||
@@ -29,6 +29,8 @@ export type SpecialKey =
|
|||||||
| "f11"
|
| "f11"
|
||||||
| "f12";
|
| "f12";
|
||||||
|
|
||||||
|
export type ModifierKey = "ctrl" | "alt";
|
||||||
|
|
||||||
const KEY_SEQUENCES: Record<SpecialKey, string> = {
|
const KEY_SEQUENCES: Record<SpecialKey, string> = {
|
||||||
escape: "\x1B",
|
escape: "\x1B",
|
||||||
tab: "\t",
|
tab: "\t",
|
||||||
@@ -59,22 +61,89 @@ const KEY_SEQUENCES: Record<SpecialKey, string> = {
|
|||||||
f12: "\x1B[24~",
|
f12: "\x1B[24~",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Single character sequences with modifier prefixes
|
||||||
|
const MODIFIER_PREFIXES: Record<string, { ctrl: string; alt: string; ctrlAlt: string }> = {
|
||||||
|
// Letters
|
||||||
|
a: { ctrl: "\x01", alt: "\x1Ba", ctrlAlt: "\x1B\x01" },
|
||||||
|
b: { ctrl: "\x02", alt: "\x1Bb", ctrlAlt: "\x1B\x02" },
|
||||||
|
c: { ctrl: "\x03", alt: "\x1Bc", ctrlAlt: "\x1B\x03" },
|
||||||
|
d: { ctrl: "\x04", alt: "\x1Bd", ctrlAlt: "\x1B\x04" },
|
||||||
|
e: { ctrl: "\x05", alt: "\x1Be", ctrlAlt: "\x1B\x05" },
|
||||||
|
f: { ctrl: "\x06", alt: "\x1Bf", ctrlAlt: "\x1B\x06" },
|
||||||
|
g: { ctrl: "\x07", alt: "\x1Bg", ctrlAlt: "\x1B\x07" },
|
||||||
|
h: { ctrl: "\x08", alt: "\x1Bh", ctrlAlt: "\x1B\x08" },
|
||||||
|
i: { ctrl: "\x09", alt: "\x1Bi", ctrlAlt: "\x1B\x09" },
|
||||||
|
j: { ctrl: "\x0A", alt: "\x1Bj", ctrlAlt: "\x1B\x0A" },
|
||||||
|
k: { ctrl: "\x0B", alt: "\x1Bk", ctrlAlt: "\x1B\x0B" },
|
||||||
|
l: { ctrl: "\x0C", alt: "\x1Bl", ctrlAlt: "\x1B\x0C" },
|
||||||
|
m: { ctrl: "\x0D", alt: "\x1Bm", ctrlAlt: "\x1B\x0D" },
|
||||||
|
n: { ctrl: "\x0E", alt: "\x1Bn", ctrlAlt: "\x1B\x0E" },
|
||||||
|
o: { ctrl: "\x0F", alt: "\x1Bo", ctrlAlt: "\x1B\x0F" },
|
||||||
|
p: { ctrl: "\x10", alt: "\x1Bp", ctrlAlt: "\x1B\x10" },
|
||||||
|
q: { ctrl: "\x11", alt: "\x1Bq", ctrlAlt: "\x1B\x11" },
|
||||||
|
r: { ctrl: "\x12", alt: "\x1Br", ctrlAlt: "\x1B\x12" },
|
||||||
|
s: { ctrl: "\x13", alt: "\x1Bs", ctrlAlt: "\x1B\x13" },
|
||||||
|
t: { ctrl: "\x14", alt: "\x1Bt", ctrlAlt: "\x1B\x14" },
|
||||||
|
u: { ctrl: "\x15", alt: "\x1Bu", ctrlAlt: "\x1B\x15" },
|
||||||
|
v: { ctrl: "\x16", alt: "\x1Bv", ctrlAlt: "\x1B\x16" },
|
||||||
|
w: { ctrl: "\x17", alt: "\x1Bw", ctrlAlt: "\x1B\x17" },
|
||||||
|
x: { ctrl: "\x18", alt: "\x1Bx", ctrlAlt: "\x1B\x18" },
|
||||||
|
y: { ctrl: "\x19", alt: "\x1By", ctrlAlt: "\x1B\x19" },
|
||||||
|
z: { ctrl: "\x1A", alt: "\x1Bz", ctrlAlt: "\x1B\x1A" },
|
||||||
|
// Numbers
|
||||||
|
"0": { ctrl: "0", alt: "\x1B0", ctrlAlt: "\x1B0" },
|
||||||
|
"1": { ctrl: "1", alt: "\x1B1", ctrlAlt: "\x1B1" },
|
||||||
|
"2": { ctrl: "\x00", alt: "\x1B2", ctrlAlt: "\x1B\x00" },
|
||||||
|
"3": { ctrl: "\x1B", alt: "\x1B3", ctrlAlt: "\x1B\x1B" },
|
||||||
|
"4": { ctrl: "\x1C", alt: "\x1B4", ctrlAlt: "\x1B\x1C" },
|
||||||
|
"5": { ctrl: "\x1D", alt: "\x1B5", ctrlAlt: "\x1B\x1D" },
|
||||||
|
"6": { ctrl: "\x1E", alt: "\x1B6", ctrlAlt: "\x1B\x1E" },
|
||||||
|
"7": { ctrl: "\x1F", alt: "\x1B7", ctrlAlt: "\x1B\x1F" },
|
||||||
|
"8": { ctrl: "\x7F", alt: "\x1B8", ctrlAlt: "\x1B\x7F" },
|
||||||
|
"9": { ctrl: "9", alt: "\x1B9", ctrlAlt: "\x1B9" },
|
||||||
|
};
|
||||||
|
|
||||||
interface UseSpecialKeysOptions {
|
interface UseSpecialKeysOptions {
|
||||||
onSend: (data: string) => void;
|
onSend: (data: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSpecialKeys({ onSend }: UseSpecialKeysOptions) {
|
export function useSpecialKeys({ onSend }: UseSpecialKeysOptions) {
|
||||||
|
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(null);
|
||||||
|
|
||||||
const sendKey = useCallback(
|
const sendKey = useCallback(
|
||||||
(key: SpecialKey) => {
|
(key: SpecialKey) => {
|
||||||
const sequence = KEY_SEQUENCES[key];
|
// Handle modifier keys (one-shot)
|
||||||
if (sequence) {
|
if (key === "ctrl" || key === "alt") {
|
||||||
onSend(sequence);
|
// Toggle modifier
|
||||||
|
setActiveModifier((current) => (current === key ? null : key));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sequence = KEY_SEQUENCES[key];
|
||||||
|
if (!sequence) return;
|
||||||
|
|
||||||
|
// Check if we have an active modifier and the key is a single character
|
||||||
|
if (activeModifier && sequence.length === 1) {
|
||||||
|
const char = sequence;
|
||||||
|
const mapping = MODIFIER_PREFIXES[char.toLowerCase()];
|
||||||
|
if (mapping) {
|
||||||
|
onSend(mapping[activeModifier]);
|
||||||
|
setActiveModifier(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSend(sequence);
|
||||||
|
setActiveModifier(null);
|
||||||
},
|
},
|
||||||
[onSend]
|
[onSend, activeModifier]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { sendKey };
|
const clearModifier = useCallback(() => {
|
||||||
|
setActiveModifier(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { sendKey, activeModifier, clearModifier };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { KEY_SEQUENCES };
|
export { KEY_SEQUENCES };
|
||||||
|
|||||||
@@ -3119,6 +3119,14 @@ a.nav-item,
|
|||||||
background: #1e5fa8;
|
background: #1e5fa8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.special-key-button.active-modifier {
|
||||||
|
background: #f5f543;
|
||||||
|
color: #1e1e1e;
|
||||||
|
border-color: #f5f543;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 0 8px rgba(245, 245, 67, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/* Special Keys Panel */
|
/* Special Keys Panel */
|
||||||
.special-keys-panel-overlay {
|
.special-keys-panel-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user