Game UI
Key hint
A keycap and what it does: "E Etkileşim", "ESC Geri".
stablev0.3.2 Chromium 103 ui-scale safe
Install
Copies the file into
components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.bunx shadcn@3 add @fastrp/key-hintExamples
Default
EEtkileşimGKapıyı kilitleSHIFTEArabayı çalıştır
ESCGeriENTERSeçKullanAyrıntıQESekme
F1TABSPACEE
API
<Kbd />One key: `E`, `ESC`, `SHIFT`, or a mouse input as `LMB`, `RMB`, `MMB` / `WHEEL`, `MOUSE`.
| Prop | Type | Default | Notes |
|---|---|---|---|
| size | "sm" | "md" | "lg" | — | |
| variant | "solid" | "cap" | "bare" | — |
<KeyHint />A key and what it does — "E Etkileşim", "ESC Çık" — the prompt every game menu speaks in. Put a row of them in a `KeyHintBar`.
| Prop | Type | Default | Notes |
|---|---|---|---|
| keys* | string | string[] | — | One key, or a chord: `['SHIFT', 'E']`. |
| label* | ReactNode | — | |
| size | "sm" | "md" | "lg" | — | |
| variant | "solid" | "cap" | "bare" | — | |
| voice | "menu" | "ui" | ui | `ui`: upright, for panels and the web. `menu`: italic, solid keys — the in-game menu's own hint, as `MenuScreen` uses it. |
<KeyHintBar />A row of key hints, spaced as a menu footer.
| Prop | Type | Default | Notes |
|---|---|---|---|
| separated | boolean | false | A slash between hints, as the in-game menu sets them. |
<MouseGlyph />A mouse with the button to press filled in — the glyph game menus use instead of the word. Drawn in `currentColor`, sized by the text around it.
| Prop | Type | Default | Notes |
|---|---|---|---|
| button* | MouseButton | — |
Dependencies
npm
- class-variance-authority@^0.7.1
Registry
- @fastrp/utils
Source
components/ds/key-hint.tsxShowHide
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
const kbdVariants = cva(
'inline-flex shrink-0 select-none items-center justify-center font-sans font-bold not-italic leading-none tracking-[0.04em] [&_svg]:h-[1.15em] [&_svg]:w-auto',
{
variants: {
variant: {
/** A keycap: a lit edge and a darker lip, as the key itself. */
cap: 'rounded-[0.3125rem] border border-foreground/25 bg-foreground/10 text-foreground shadow-[inset_0_-2px_0_rgb(0_0_0/0.35)]',
/** Solid white, dark letter — the in-game menu's key. */
solid: 'rounded-menu bg-foreground text-background',
/** Just the glyph, on a busy ground. */
bare: 'text-foreground',
},
size: {
sm: 'h-5 min-w-5 px-1 text-[0.6875rem]',
md: 'h-6 min-w-6 px-1.5 text-xs',
lg: 'h-8 min-w-8 px-2 text-sm',
},
},
defaultVariants: { variant: 'cap', size: 'md' },
},
);
type MouseButton = 'left' | 'right' | 'middle' | 'move';
/** Mouse inputs, by the name a hint uses for them. */
const MOUSE: Record<string, { button: MouseButton; label: string }> = {
LMB: { button: 'left', label: 'Sol tık' },
RMB: { button: 'right', label: 'Sağ tık' },
MMB: { button: 'middle', label: 'Tekerlek' },
WHEEL: { button: 'middle', label: 'Tekerlek' },
MOUSE: { button: 'move', label: 'Fare' },
};
/**
* A mouse with the button to press filled in — the glyph game menus use instead of the word. Drawn
* in `currentColor`, sized by the text around it.
*/
function MouseGlyph({ button }: { button: MouseButton }) {
return (
<svg viewBox="0 0 16 22" fill="none" aria-hidden className="h-[1.15em] w-auto">
<rect
x="1.25"
y="1.25"
width="13.5"
height="19.5"
rx="6.75"
stroke="currentColor"
strokeWidth="1.5"
/>
<path d="M8 1.5V9M1.5 9H14.5" stroke="currentColor" strokeWidth="1.5" />
{button === 'left' && (
<path d="M8 1.5C4.4 1.5 1.5 4.1 1.5 7.3V9H8V1.5Z" fill="currentColor" />
)}
{button === 'right' && (
<path d="M8 1.5C11.6 1.5 14.5 4.1 14.5 7.3V9H8V1.5Z" fill="currentColor" />
)}
{button === 'middle' && (
<rect x="6.5" y="3" width="3" height="5" rx="1.5" fill="currentColor" />
)}
{button === 'move' && (
<path
d="M8 12.5V17.5M5.5 15H10.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
)}
</svg>
);
}
export type KbdProps = React.ComponentProps<'kbd'> & VariantProps<typeof kbdVariants>;
/** One key: `E`, `ESC`, `SHIFT`, or a mouse input as `LMB`, `RMB`, `MMB` / `WHEEL`, `MOUSE`. */
function Kbd({ variant, size, className, children, ...props }: KbdProps) {
const mouse = typeof children === 'string' ? MOUSE[children] : undefined;
return (
<kbd
data-slot="kbd"
// A mouse glyph needs no cap around it: it is its own shape.
className={cn(kbdVariants({ variant: mouse ? 'bare' : variant, size }), className)}
aria-label={mouse?.label}
{...props}
>
{mouse ? <MouseGlyph button={mouse.button} /> : children}
</kbd>
);
}
export type KeyHintProps = Omit<React.ComponentProps<'span'>, 'children'> & {
/** One key, or a chord: `['SHIFT', 'E']`. */
keys: string | string[];
label: React.ReactNode;
variant?: KbdProps['variant'];
size?: KbdProps['size'];
/**
* `ui`: upright, for panels and the web. `menu`: italic, solid keys — the in-game menu's own
* hint, as `MenuScreen` uses it.
*/
voice?: 'ui' | 'menu';
};
/**
* A key and what it does — "E Etkileşim", "ESC Çık" — the prompt every game menu speaks in. Put
* a row of them in a `KeyHintBar`.
*/
function KeyHint({ keys, label, variant, size, voice = 'ui', className, ...props }: KeyHintProps) {
const list = Array.isArray(keys) ? keys : [keys];
return (
<span
data-slot="key-hint"
className={cn(
'inline-flex items-center gap-2 text-foreground/90',
voice === 'ui' ? 'font-display text-label uppercase' : 'font-menu text-menu-hint',
className,
)}
{...props}
>
<span className="inline-flex items-center gap-1">
{list.map((key) => (
<Kbd key={key} variant={variant ?? (voice === 'menu' ? 'solid' : 'cap')} size={size}>
{key}
</Kbd>
))}
</span>
{label}
</span>
);
}
export type KeyHintBarProps = React.ComponentProps<'div'> & {
/** A slash between hints, as the in-game menu sets them. */
separated?: boolean;
};
/** A row of key hints, spaced as a menu footer. */
function KeyHintBar({ separated = false, className, children, ...props }: KeyHintBarProps) {
const items = React.Children.toArray(children);
return (
<div
data-slot="key-hint-bar"
className={cn(
'flex flex-wrap items-center gap-y-2',
separated ? 'gap-x-3' : 'gap-x-5',
className,
)}
{...props}
>
{separated
? items.map((item, index) => (
<React.Fragment key={(item as React.ReactElement).key ?? index}>
{index > 0 && (
<span aria-hidden className="font-menu text-menu-hint text-foreground/40">
/
</span>
)}
{item}
</React.Fragment>
))
: items}
</div>
);
}
export { Kbd, KeyHint, KeyHintBar, kbdVariants, MouseGlyph };