Add configurable hotkeys support (#343)

This commit is contained in:
Gregory Schier
2026-01-04 08:36:22 -08:00
committed by GitHub
parent 58bf55704a
commit 00bf5920e3
23 changed files with 540 additions and 79 deletions

View File

@@ -34,7 +34,7 @@ import { jotaiStore } from '../../lib/jotai';
import { ErrorBoundary } from '../ErrorBoundary';
import { Overlay } from '../Overlay';
import { Button } from './Button';
import { HotKey } from './HotKey';
import { Hotkey } from './Hotkey';
import { Icon } from './Icon';
import { LoadingIcon } from './LoadingIcon';
import { Separator } from './Separator';
@@ -630,7 +630,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
[focused],
);
const rightSlot = item.rightSlot ?? <HotKey action={item.hotKeyAction ?? null} />;
const rightSlot = item.rightSlot ?? <Hotkey action={item.hotKeyAction ?? null} />;
return (
<Button

View File

@@ -9,23 +9,34 @@ interface Props {
variant?: 'text' | 'with-bg';
}
export function HotKey({ action, className, variant }: Props) {
export function Hotkey({ action, className, variant }: Props) {
const labelParts = useFormattedHotkey(action);
if (labelParts === null) {
return null;
}
return <HotkeyRaw labelParts={labelParts} className={className} variant={variant} />;
}
interface HotkeyRawProps {
labelParts: string[];
className?: string;
variant?: 'text' | 'with-bg';
}
export function HotkeyRaw({ labelParts, className, variant }: HotkeyRawProps) {
return (
<HStack
className={classNames(
className,
variant === 'with-bg' && 'rounded border',
'text-text-subtlest',
variant === 'with-bg' &&
'rounded bg-surface-highlight px-1 border border-border text-text-subtle',
variant === 'text' && 'text-text-subtlest',
)}
>
{labelParts.map((char, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<div key={index} className="min-w-[1.1em] text-center">
<div key={index} className="min-w-[1em] text-center">
{char}
</div>
))}

View File

@@ -1,14 +1,14 @@
import classNames from 'classnames';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { useHotKeyLabel } from '../../hooks/useHotKey';
import { useHotkeyLabel } from '../../hooks/useHotKey';
interface Props {
action: HotkeyAction;
className?: string;
}
export function HotKeyLabel({ action, className }: Props) {
const label = useHotKeyLabel(action);
export function HotkeyLabel({ action, className }: Props) {
const label = useHotkeyLabel(action);
return (
<span className={classNames(className, 'text-text-subtle whitespace-nowrap')}>{label}</span>
);

View File

@@ -2,8 +2,8 @@ import classNames from 'classnames';
import type { ReactNode } from 'react';
import { Fragment } from 'react';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { HotKey } from './HotKey';
import { HotKeyLabel } from './HotKeyLabel';
import { Hotkey } from './Hotkey';
import { HotkeyLabel } from './HotkeyLabel';
interface Props {
hotkeys: HotkeyAction[];
@@ -11,14 +11,14 @@ interface Props {
className?: string;
}
export const HotKeyList = ({ hotkeys, bottomSlot, className }: Props) => {
export const HotkeyList = ({ hotkeys, bottomSlot, className }: Props) => {
return (
<div className={classNames(className, 'h-full flex items-center justify-center')}>
<div className="grid gap-2 grid-cols-[auto_auto]">
{hotkeys.map((hotkey) => (
<Fragment key={hotkey}>
<HotKeyLabel className="truncate" action={hotkey} />
<HotKey className="ml-4" action={hotkey} />
<HotkeyLabel className="truncate" action={hotkey} />
<Hotkey className="ml-4" action={hotkey} />
</Fragment>
))}
{bottomSlot}

View File

@@ -127,6 +127,7 @@ import {
UploadIcon,
VariableIcon,
Wand2Icon,
WifiIcon,
WrenchIcon,
XIcon,
} from 'lucide-react';
@@ -260,6 +261,7 @@ const icons = {
update: RefreshCcwIcon,
upload: UploadIcon,
variable: VariableIcon,
wifi: WifiIcon,
wrench: WrenchIcon,
x: XIcon,
_unknown: ShieldAlertIcon,

View File

@@ -51,12 +51,21 @@ export function TableRow({ children }: { children: ReactNode }) {
return <tr>{children}</tr>;
}
export function TableCell({ children, className }: { children: ReactNode; className?: string }) {
export function TableCell({
children,
className,
align = 'left',
}: {
children: ReactNode;
className?: string;
align?: 'left' | 'center' | 'right';
}) {
return (
<td
className={classNames(
className,
'py-2 [&:not(:first-child)]:pl-4 text-left whitespace-nowrap',
'py-2 [&:not(:first-child)]:pl-4 whitespace-nowrap',
align === 'left' ? 'text-left' : align === 'center' ? 'text-center' : 'text-right',
)}
>
{children}

View File

@@ -13,11 +13,13 @@ export type TabItem =
value: string;
label: string;
hidden?: boolean;
leftSlot?: ReactNode;
rightSlot?: ReactNode;
}
| {
value: string;
options: Omit<RadioDropdownProps, 'children'>;
leftSlot?: ReactNode;
rightSlot?: ReactNode;
};
@@ -95,7 +97,7 @@ export function Tabs({
>
<div
className={classNames(
layout === 'horizontal' && 'flex flex-col gap-1 w-full pb-3 mb-auto',
layout === 'horizontal' && 'flex flex-col w-full pb-3 mb-auto',
layout === 'vertical' && 'flex flex-row flex-shrink-0 gap-2 w-full',
)}
>
@@ -107,7 +109,6 @@ export function Tabs({
const isActive = t.value === value;
const btnProps: Partial<ButtonProps> = {
size: 'sm',
color: 'custom',
justify: layout === 'horizontal' ? 'start' : 'center',
onClick: isActive ? undefined : () => onChangeValue(t.value),
@@ -142,6 +143,7 @@ export function Tabs({
onChange={t.options.onChange}
>
<Button
leftSlot={t.leftSlot}
rightSlot={
<div className="flex items-center">
{t.rightSlot}
@@ -165,7 +167,7 @@ export function Tabs({
);
}
return (
<Button key={t.value} rightSlot={t.rightSlot} {...btnProps}>
<Button key={t.value} leftSlot={t.leftSlot} rightSlot={t.rightSlot} {...btnProps}>
{t.label}
</Button>
);