feat: add Kbd component for keyboard shortcuts and integrate with Tooltip

- Introduced a new Kbd component to display keyboard shortcuts, supporting both single keys and key combinations.
- Added KbdGroup for grouping multiple Kbd components together.
- Updated package.json to include @radix-ui/react-tooltip version 1.2.8.
- Created stories for Kbd component showcasing various use cases, including integration with Tooltip for enhanced user guidance.
This commit is contained in:
MyPrototypeWhat 2025-11-18 16:50:10 +08:00
parent 5fdfa5a594
commit 583e4e9db7
5 changed files with 633 additions and 0 deletions

View File

@ -54,6 +54,7 @@
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

View File

@ -57,6 +57,7 @@ export * from './primitives/checkbox'
export * from './primitives/combobox'
export * from './primitives/command'
export * from './primitives/dialog'
export * from './primitives/kbd'
export * from './primitives/popover'
export * from './primitives/radioGroup'
export * from './primitives/select'

View File

@ -0,0 +1,22 @@
import { cn } from '@cherrystudio/ui/utils/index'
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return (
<kbd
data-slot="kbd"
className={cn(
'bg-primary/10 text-primary pointer-events-none inline-flex w-fit min-w-5 items-center justify-center gap-1 rounded-3xs p-1 font-sans text-xs font-medium select-none',
"[&_svg:not([class*='size-'])]:size-3",
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
return <kbd data-slot="kbd-group" className={cn('inline-flex items-center gap-1', className)} {...props} />
}
export { Kbd, KbdGroup }

View File

@ -0,0 +1,578 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Command, Copy, Save, Search } from 'lucide-react'
import { Kbd, KbdGroup } from '../../../src/components/primitives/kbd'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../src/components/primitives/tooltip'
const meta: Meta<typeof Kbd> = {
title: 'Components/Primitives/Kbd',
component: Kbd,
parameters: {
layout: 'centered',
docs: {
description: {
component: '用于显示键盘快捷键的组件,支持单个按键和组合快捷键'
}
}
},
tags: ['autodocs'],
argTypes: {
className: {
control: { type: 'text' },
description: '自定义 CSS 类名'
},
children: {
control: { type: 'text' },
description: '键盘按键内容'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// 基础示例
export const Default: Story = {
args: {
children: 'Ctrl'
}
}
// 单个按键
export const SingleKeys: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
<Kbd>Ctrl</Kbd>
<Kbd>Shift</Kbd>
<Kbd>Alt</Kbd>
<Kbd>Enter</Kbd>
<Kbd>Esc</Kbd>
<Kbd>Tab</Kbd>
<Kbd>Space</Kbd>
<Kbd>Delete</Kbd>
</div>
)
}
// 字母和数字按键
export const AlphanumericKeys: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
<Kbd>A</Kbd>
<Kbd>B</Kbd>
<Kbd>C</Kbd>
<Kbd>1</Kbd>
<Kbd>2</Kbd>
<Kbd>3</Kbd>
<Kbd>F1</Kbd>
<Kbd>F2</Kbd>
<Kbd>F12</Kbd>
</div>
)
}
// 方向键
export const ArrowKeys: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
</div>
)
}
// 组合快捷键
export const KeyCombinations: Story = {
render: () => (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>S</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>C</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>V</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>F</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>A</Kbd>
</KbdGroup>
</div>
</div>
)
}
// Mac 快捷键
export const MacKeys: Story = {
render: () => (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd></Kbd>
<Kbd>S</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd></Kbd>
<Kbd>C</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd></Kbd>
<Kbd>V</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd>4</Kbd>
</KbdGroup>
</div>
</div>
)
}
// 三键组合
export const ThreeKeyCombinations: Story = {
render: () => (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<span className="w-32 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Shift</Kbd>
<Kbd>Z</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-32 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Alt</Kbd>
<Kbd>Z</Kbd>
</KbdGroup>
</div>
<div className="flex items-center gap-2">
<span className="w-32 text-sm text-muted-foreground">:</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Shift</Kbd>
<Kbd>F</Kbd>
</KbdGroup>
</div>
</div>
)
}
// 带图标的按键
export const WithIcons: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
<Kbd>
<Command />
</Kbd>
<Kbd>
<Copy />
</Kbd>
<Kbd>
<Save />
</Kbd>
<Kbd>
<Search />
</Kbd>
</div>
)
}
// 在 Tooltip 中使用
export const InTooltip: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">
</button>
</TooltipTrigger>
<TooltipContent>
<Kbd>Ctrl+S</Kbd>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="rounded bg-secondary px-4 py-2 text-sm font-medium text-secondary-foreground hover:bg-secondary/80">
</button>
</TooltipTrigger>
<TooltipContent>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>C</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="rounded bg-secondary px-4 py-2 text-sm font-medium text-secondary-foreground hover:bg-secondary/80">
</button>
</TooltipTrigger>
<TooltipContent>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>V</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</div>
)
}
// 快捷键列表
export const ShortcutList: Story = {
render: () => (
<div className="w-96 space-y-2 rounded-lg border p-4">
<h3 className="mb-3 text-base font-semibold"></h3>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>S</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>O</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>F</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>H</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Z</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Y</Kbd>
</KbdGroup>
</div>
</div>
</div>
)
}
// 编辑器快捷键
export const EditorShortcuts: Story = {
render: () => (
<div className="w-[600px] space-y-4 rounded-lg border p-6">
<h3 className="text-lg font-semibold"></h3>
<div className="space-y-3">
<div>
<h4 className="mb-2 text-sm font-medium text-muted-foreground"></h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>N</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>O</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>S</Kbd>
</KbdGroup>
</div>
</div>
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-muted-foreground"></h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>C</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>X</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>V</Kbd>
</KbdGroup>
</div>
</div>
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-muted-foreground"></h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>G</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>F</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Shift</Kbd>
<Kbd>F</Kbd>
</KbdGroup>
</div>
</div>
</div>
</div>
</div>
)
}
// 游戏控制
export const GameControls: Story = {
render: () => (
<div className="w-96 space-y-4 rounded-lg border p-6">
<h3 className="text-lg font-semibold"></h3>
<div className="space-y-3">
<div>
<h4 className="mb-2 text-sm font-medium text-muted-foreground"></h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Kbd>W</Kbd>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Kbd>S</Kbd>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Kbd>A</Kbd>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Kbd>D</Kbd>
</div>
</div>
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-muted-foreground"></h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Kbd>Space</Kbd>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Kbd>Shift</Kbd>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">使</span>
<Kbd>E</Kbd>
</div>
</div>
</div>
</div>
</div>
)
}
// 特殊字符
export const SpecialCharacters: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
</div>
)
}
// 不同尺寸 (通过自定义类名)
export const CustomSizes: Story = {
render: () => (
<div className="flex items-center gap-3">
<Kbd className="h-4 min-w-4 text-[10px]">S</Kbd>
<Kbd>M</Kbd>
<Kbd className="h-6 min-w-6 text-sm">L</Kbd>
<Kbd className="h-8 min-w-8 text-base">XL</Kbd>
</div>
)
}
// 实际应用示例
export const RealWorldExample: Story = {
render: () => (
<div className="w-[700px] space-y-6">
<div className="rounded-lg border p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold"></h3>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded-md p-2 hover:bg-muted">
<div className="flex items-center gap-3">
<Save className="h-4 w-4" />
<span className="text-sm"></span>
</div>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>S</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between rounded-md p-2 hover:bg-muted">
<div className="flex items-center gap-3">
<Copy className="h-4 w-4" />
<span className="text-sm"></span>
</div>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>C</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between rounded-md p-2 hover:bg-muted">
<div className="flex items-center gap-3">
<Search className="h-4 w-4" />
<span className="text-sm"></span>
</div>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>F</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between rounded-md p-2 hover:bg-muted">
<div className="flex items-center gap-3">
<Command className="h-4 w-4" />
<span className="text-sm"></span>
</div>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Shift</Kbd>
<Kbd>P</Kbd>
</KbdGroup>
</div>
</div>
</div>
<div className="rounded-lg border p-6">
<h3 className="mb-4 text-lg font-semibold"></h3>
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
<Kbd>Ctrl</Kbd>
</p>
<p className="text-sm text-muted-foreground">
使{' '}
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd></Kbd>
</KbdGroup>{' '}
{' '}
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd></Kbd>
</KbdGroup>{' '}
</p>
<p className="text-sm text-muted-foreground">
<Kbd>Enter</Kbd> ,<Kbd>Esc</Kbd>
</p>
</div>
</div>
</div>
)
}

View File

@ -2257,6 +2257,7 @@ __metadata:
"@radix-ui/react-radio-group": "npm:^1.3.8"
"@radix-ui/react-select": "npm:^2.2.6"
"@radix-ui/react-slot": "npm:^1.2.3"
"@radix-ui/react-tooltip": "npm:^1.2.8"
"@radix-ui/react-use-controllable-state": "npm:^1.2.2"
"@storybook/addon-docs": "npm:^10.0.5"
"@storybook/addon-themes": "npm:^10.0.5"
@ -7631,6 +7632,36 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-tooltip@npm:^1.2.8":
version: 1.2.8
resolution: "@radix-ui/react-tooltip@npm:1.2.8"
dependencies:
"@radix-ui/primitive": "npm:1.1.3"
"@radix-ui/react-compose-refs": "npm:1.1.2"
"@radix-ui/react-context": "npm:1.1.2"
"@radix-ui/react-dismissable-layer": "npm:1.1.11"
"@radix-ui/react-id": "npm:1.1.1"
"@radix-ui/react-popper": "npm:1.2.8"
"@radix-ui/react-portal": "npm:1.1.9"
"@radix-ui/react-presence": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.3"
"@radix-ui/react-slot": "npm:1.2.3"
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
"@radix-ui/react-visually-hidden": "npm:1.2.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10c0/de0cbae9c571a00671f160928d819e59502f59be8749f536ab4b180181d9d70aee3925a5b2555f8f32d0bea622bc35f65b70ca7ff0449e4844f891302310cc48
languageName: node
linkType: hard
"@radix-ui/react-use-callback-ref@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1"