adaedb70ef
Mobile Navigation: - Add MobileNav component with bottom tab bar - Show mobile nav on small screens, hide desktop sidebar - Add session count badge to Sessions tab - Add safe area padding for notched devices Session Management: - Redesign SessionCard for mobile with action menu - Add MobileActionSheet for session actions - Keep primary action prominent Forms & Dialogs: - Stack form fields vertically on mobile - Ensure 44px minimum touch targets - Update dialogs for 320px viewport Responsive Layout: - Add MobilePageHeader with back button - Reduce page padding on mobile - Stack multi-column grids vertically Touch & Interaction: - Add active states to interactive elements - Ensure 8px spacing between touch targets Complex Pages: - Update Repo Workspace for mobile - Update Tool Workshop and Config Profiles Build: TypeScript check passes, production build succeeds
97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
import React from "react";
|
|
import { getSequenceWithModifier, type SpecialKey, type ModifierKey } from "../hooks/use-special-keys";
|
|
|
|
interface SpecialKeysStripProps {
|
|
onSend: (data: string) => void;
|
|
isVisible: boolean;
|
|
onMoreClick?: () => void;
|
|
onKeepFocus?: () => void;
|
|
activeModifier: ModifierKey | null;
|
|
onModifierChange: (modifier: ModifierKey | null) => void;
|
|
}
|
|
|
|
const PRIMARY_KEYS: { key: SpecialKey; label: string; isModifier?: boolean }[] = [
|
|
{ key: "escape", label: "Esc" },
|
|
{ key: "tab", label: "Tab" },
|
|
{ key: "ctrl", label: "Ctrl", isModifier: true },
|
|
{ key: "alt", label: "Alt", isModifier: true },
|
|
{ key: "up", label: "↑" },
|
|
{ key: "down", label: "↓" },
|
|
{ key: "left", label: "←" },
|
|
{ key: "right", label: "→" },
|
|
];
|
|
|
|
export const SpecialKeysStrip: React.FC<SpecialKeysStripProps> = ({
|
|
onSend,
|
|
isVisible,
|
|
onMoreClick,
|
|
onKeepFocus,
|
|
activeModifier,
|
|
onModifierChange,
|
|
}) => {
|
|
const handlePointerDown = (e: React.PointerEvent, key: SpecialKey) => {
|
|
e.preventDefault();
|
|
|
|
// Handle modifier keys (one-shot)
|
|
if (key === "ctrl" || key === "alt") {
|
|
onModifierChange(activeModifier === key ? null : key);
|
|
requestAnimationFrame(() => {
|
|
onKeepFocus?.();
|
|
});
|
|
return;
|
|
}
|
|
|
|
const result = getSequenceWithModifier(key, activeModifier);
|
|
if (result) {
|
|
onSend(result.sequence);
|
|
if (result.clearModifier) {
|
|
onModifierChange(null);
|
|
}
|
|
}
|
|
|
|
// Always refocus terminal after sending
|
|
requestAnimationFrame(() => {
|
|
onKeepFocus?.();
|
|
});
|
|
};
|
|
|
|
const handleMorePointerDown = (e: React.PointerEvent) => {
|
|
e.preventDefault();
|
|
onMoreClick?.();
|
|
requestAnimationFrame(() => {
|
|
onKeepFocus?.();
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className={`special-keys-strip ${isVisible ? "visible" : "hidden"}`}>
|
|
{PRIMARY_KEYS.map(({ key, label, isModifier }) => (
|
|
<button
|
|
key={key}
|
|
className={`special-key-button ${
|
|
isModifier && activeModifier === key ? "active-modifier" : ""
|
|
}`}
|
|
onPointerDown={(e) => handlePointerDown(e, key)}
|
|
type="button"
|
|
tabIndex={-1}
|
|
aria-label={`Send ${label}`}
|
|
aria-pressed={isModifier && activeModifier === key}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
{onMoreClick && (
|
|
<button
|
|
className="special-key-button special-key-more"
|
|
onPointerDown={handleMorePointerDown}
|
|
type="button"
|
|
tabIndex={-1}
|
|
aria-label="More special keys"
|
|
>
|
|
More
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|