-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add Kbd component #886
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| 'use client'; | ||
|
|
||
| import type { ComponentPropsType } from '@/components/demo/types'; | ||
| import { getPropsString } from '@/lib/utils'; | ||
|
|
||
| export const getCode = (props: ComponentPropsType) => { | ||
| const { children, ...rest } = props; | ||
|
|
||
| return `<Kbd${getPropsString(rest)}>${children}</Kbd>`; | ||
| }; | ||
|
|
||
| export const playground = { | ||
| type: 'playground', | ||
| controls: { | ||
| variant: { | ||
| type: 'select', | ||
| options: ['solid', 'ghost'], | ||
| defaultValue: 'solid' | ||
| }, | ||
| children: { | ||
| type: 'text', | ||
| initialValue: 'Esc' | ||
| } | ||
| }, | ||
| getCode | ||
| }; | ||
|
|
||
| export const singleDemo = { | ||
| type: 'code', | ||
| code: `<Flex gap={5} align="center"> | ||
| <Kbd>Esc</Kbd> | ||
| <Kbd aria-label="Command">⌘</Kbd> | ||
| <Kbd aria-label="Shift">⇧</Kbd> | ||
| <Kbd aria-label="Enter">↵</Kbd> | ||
| <Kbd>Tab</Kbd> | ||
| </Flex>` | ||
| }; | ||
|
|
||
| export const variantDemo = { | ||
| type: 'code', | ||
| code: `<Flex gap={7} align="center"> | ||
| <Kbd.Group> | ||
| <Kbd aria-label="Command">⌘</Kbd> | ||
| <Kbd>K</Kbd> | ||
| </Kbd.Group> | ||
| <Kbd.Group variant="ghost"> | ||
| <Kbd aria-label="Command">⌘</Kbd> | ||
| <Kbd>K</Kbd> | ||
| </Kbd.Group> | ||
| </Flex>` | ||
| }; | ||
|
|
||
| export const groupDemo = { | ||
| type: 'code', | ||
| code: `<Flex gap={7} align="center"> | ||
| <Kbd.Group> | ||
| <Kbd aria-label="Command">⌘</Kbd> | ||
| <Kbd>K</Kbd> | ||
| </Kbd.Group> | ||
| <Kbd.Group> | ||
| <Kbd aria-label="Command">⌘</Kbd> | ||
| <Kbd aria-label="Shift">⇧</Kbd> | ||
| <Kbd>P</Kbd> | ||
| </Kbd.Group> | ||
| </Flex>` | ||
| }; | ||
|
|
||
| export const separatorDemo = { | ||
| type: 'code', | ||
| tabs: [ | ||
| { | ||
| name: 'Plus', | ||
| code: `<Kbd.Group> | ||
| <Kbd aria-label="Command">⌘</Kbd> | ||
| + | ||
| <Kbd>K</Kbd> | ||
| </Kbd.Group>` | ||
| }, | ||
| { | ||
| name: 'Then', | ||
| code: `<Kbd.Group> | ||
| <Kbd>G</Kbd> | ||
| then | ||
| <Kbd>P</Kbd> | ||
| </Kbd.Group>` | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| export const withTextDemo = { | ||
| type: 'code', | ||
| code: `<Text size="small" variant="secondary"> | ||
| Press <Kbd.Group><Kbd aria-label="Command">⌘</Kbd><Kbd>K</Kbd></Kbd.Group> to open the command palette. | ||
| </Text>` | ||
| }; | ||
|
|
||
| export const withInputDemo = { | ||
| type: 'code', | ||
| code: `<Input | ||
| placeholder="Search projects" | ||
| trailingIcon={<Kbd variant="ghost" aria-label="Command K">⌘K</Kbd>} | ||
| />` | ||
| }; | ||
|
|
||
| export const withTooltipDemo = { | ||
| type: 'code', | ||
| code: `<Tooltip> | ||
| <Tooltip.Trigger render={<Button variant="outline" />}> | ||
| Search | ||
| </Tooltip.Trigger> | ||
| <Tooltip.Content> | ||
| <Flex gap={3} align="center"> | ||
| Open search | ||
| <Kbd.Group variant="ghost"> | ||
| <Kbd aria-label="Command">⌘</Kbd> | ||
| <Kbd>K</Kbd> | ||
| </Kbd.Group> | ||
| </Flex> | ||
| </Tooltip.Content> | ||
| </Tooltip>` | ||
| }; |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a playground too since we are introducing variants now |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| --- | ||
| title: Kbd | ||
| description: A component for displaying keyboard keys and shortcuts. | ||
| source: packages/raystack/components/kbd | ||
| tag: new | ||
| --- | ||
|
|
||
| import { | ||
| playground, | ||
| singleDemo, | ||
| variantDemo, | ||
| groupDemo, | ||
| separatorDemo, | ||
| withTextDemo, | ||
| withInputDemo, | ||
| withTooltipDemo, | ||
| } from "./demo.ts"; | ||
|
|
||
| <Demo data={playground} /> | ||
|
|
||
| ## Anatomy | ||
|
|
||
| Import and assemble the component. A single `Kbd` renders one key; wrap several in `Kbd.Group` to show a sequence. | ||
|
|
||
| ```tsx | ||
| import { Kbd } from "@raystack/apsara"; | ||
|
|
||
| <Kbd>Esc</Kbd> | ||
|
|
||
| <Kbd.Group> | ||
| <Kbd>⌘</Kbd> | ||
| <Kbd>K</Kbd> | ||
| </Kbd.Group> | ||
| ``` | ||
|
|
||
| ## API Reference | ||
|
|
||
| Both parts render a `<kbd>` element and forward any native attributes (`id`, `title`, `aria-label`, …) to it. | ||
|
|
||
| ### Root | ||
|
|
||
| A single keyboard key. Renders a `<kbd>` element. | ||
|
|
||
| <auto-type-table path="./props.ts" name="KbdProps" /> | ||
|
|
||
| ### Group | ||
|
|
||
| Groups multiple keyboard keys for key combinations. | ||
|
|
||
| <auto-type-table path="./props.ts" name="KbdGroupProps" /> | ||
|
|
||
| ### Slots | ||
|
|
||
| Every rendered part carries a stable `data-slot` attribute for [styling and testing](/docs/styling#with-data-slot): | ||
|
|
||
| | Slot | Element | | ||
| |------|---------| | ||
| | `kbd` | Each individual key | | ||
| | `kbd-group` | The `Kbd.Group` wrapper | | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Single keys | ||
|
|
||
| Use `Kbd` on its own for a one-key hint. Keys share a minimum width so a narrow `K` lines up with a wide `⌘`. | ||
|
|
||
| <Demo data={singleDemo} /> | ||
|
|
||
| ### Variants | ||
|
|
||
| `solid` is the default and suits standalone hints. Use `ghost` on surfaces that already have their own background, such as a menu row, a tooltip, or an input. | ||
|
|
||
| <Demo data={variantDemo} /> | ||
|
|
||
| ### Sequences | ||
|
|
||
| Wrap keys in `Kbd.Group` to show a chord. Setting `variant` on the group applies it to every key inside. | ||
|
|
||
| <Demo data={groupDemo} /> | ||
|
|
||
| ### Separators | ||
|
|
||
| `Kbd.Group` renders whatever you put between the keys, so separators are plain text. Use `+` for keys pressed together and a word like `then` for keys pressed in order. Separator text takes the surrounding typography rather than the key styling. | ||
|
|
||
| <Demo data={separatorDemo} /> | ||
|
|
||
| ### Inline with text | ||
|
|
||
| Keys sit on the text baseline, so they can be dropped straight into a sentence. | ||
|
|
||
| <Demo data={withTextDemo} /> | ||
|
Comment on lines
+87
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even tho the example says Inline with text, the actual example is rendering in side a Flex, so it's misleading |
||
|
|
||
| ### In an input | ||
|
|
||
| Surface a focus shortcut in a search field. Use a single `ghost` key here — the input's trailing slot is sized for an icon, so a multi-key `Kbd.Group` will be clipped. | ||
|
|
||
| <Demo data={withInputDemo} /> | ||
|
|
||
| ### In a tooltip | ||
|
|
||
| A common use is surfacing a shortcut alongside the action it triggers. | ||
|
|
||
| <Demo data={withTooltipDemo} /> | ||
|
|
||
| ## Accessibility | ||
|
|
||
| - `Kbd` is presentational and renders the semantic `<kbd>` element. It has no ARIA role of its own and is not exposed as a separate accessible object, so it does not change how surrounding content is announced. | ||
| - Symbol-only keys such as `⌘`, `⇧`, or `↵` are not announced usefully on their own — they are read by their Unicode names, if at all. Add an `aria-label` when the symbol is the only cue: `<Kbd aria-label="Command">⌘</Kbd>`. | ||
| - Keys are not focusable and carry no interaction. Keep the shortcut wired to a real handler elsewhere — `Kbd` only displays it. | ||
| - Keys ignore pointer events and text selection, so clicking or dragging across a menu row does not highlight the key labels. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import type { ReactNode } from 'react'; | ||
|
|
||
| export interface KbdProps { | ||
| /** The key to display, e.g. `⌘`, `Esc`, or `Enter`. */ | ||
| children?: ReactNode; | ||
|
|
||
| /** | ||
| * Visual style variant. Inherited from a parent `Kbd.Group` when set there. | ||
| * @defaultValue "solid" | ||
| */ | ||
| variant?: 'solid' | 'ghost'; | ||
|
Comment on lines
+7
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -i 'kbd|command' . | head -80
printf '%s\n' '--- Kbd props ---'
cat -n apps/www/src/content/docs/components/kbd/props.ts
printf '%s\n' '--- Command props ---'
sed -n '110,165p' apps/www/src/content/docs/components/command/props.ts
printf '%s\n' '--- relevant symbols ---'
rg -n --glob '!node_modules' 'KbdProps|KbdGroupProps|CommandShortcutProps|forwardRef|<kbd|...props|variant' apps/www/src packages src 2>/dev/null | head -240Repository: raystack/apsara Length of output: 26103 🏁 Script executed: #!/bin/bash
set -eu
for f in \
apps/www/src/components/kbd/kbd.tsx \
packages/raystack/components/kbd/kbd.tsx \
packages/raystack/components/command/command.tsx \
packages/raystack/components/command/command-misc.tsx \
apps/www/src/content/docs/components/kbd/index.mdx \
apps/www/src/content/docs/components/command/index.mdx
do
if [ -f "$f" ]; then
printf '\n--- %s (%s lines) ---\n' "$f" "$(wc -l < "$f")"
ast-grep outline "$f" || true
fi
done
printf '\n--- implementation excerpts ---\n'
cat -n apps/www/src/components/kbd/kbd.tsx
printf '\n--- package Kbd ---\n'
cat -n packages/raystack/components/kbd/kbd.tsx
printf '\n--- command references ---\n'
rg -n -C 5 'Shortcut|shortcut|Kbd' packages/raystack/components/command apps/www/src/content/docs/components/command apps/www/src/content/docs/components/kbd apps/www/src/components/kbdRepository: raystack/apsara Length of output: 50373 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- package metadata ---'
rg -n -C 3 '"react"|"typescript"|`@types/react`|auto-type-table' \
package.json pnpm-lock.yaml yarn.lock package-lock.json \
packages apps 2>/dev/null | head -220
printf '%s\n' '--- documentation prop inheritance patterns ---'
rg -n -C 2 'ComponentProps|HTMLAttributes|ButtonHTMLAttributes|extends .*Props|native attributes|forward.*ref' \
apps/www/src/content/docs/components -g 'props.ts' | head -260
printf '%s\n' '--- relevant tests ---'
sed -n '315,365p' packages/raystack/components/command/__tests__/command.test.tsx
sed -n '1,150p' packages/raystack/components/kbd/__tests__/kbd.test.tsx
printf '%s\n' '--- exports ---'
rg -n -C 3 'components/kbd|components/command|export.*Kbd|export.*Command' packages/raystack -g '*.ts' -g '*.tsx' -g 'package.json' | head -180Repository: raystack/apsara Length of output: 42826 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
docs = {
"KbdProps": Path("apps/www/src/content/docs/components/kbd/props.ts"),
"KbdGroupProps": Path("apps/www/src/content/docs/components/kbd/props.ts"),
"CommandShortcutProps": Path("apps/www/src/content/docs/components/command/props.ts"),
}
impl = Path("packages/raystack/components/kbd/kbd.tsx").read_text()
command = Path("packages/raystack/components/command/command-misc.tsx").read_text()
print("--- implementation contracts ---")
for name in ("KbdProps", "KbdGroupProps"):
m = re.search(rf"(?:export )?type {name}\s*=\s*([^;]+);", impl)
print(f"{name}: {m.group(1).strip() if m else 'NOT FOUND'}")
m = re.search(r"export type CommandShortcutProps\s*=\s*([^;]+);", command)
print(f"CommandShortcutProps: {m.group(1).strip() if m else 'NOT FOUND'}")
print("--- forwarding sites ---")
for name, text, pattern in [
("KbdRoot", impl, r"<kbd\b[\s\S]*?\{\.\.\.props\}"),
("KbdGroup", impl, r"<kbd\b[\s\S]*?\{\.\.\.props\}"),
("CommandShortcut", command, r"<Kbd\.Group[\s\S]*?\{\.\.\.props\}"),
]:
print(f"{name}: {'yes' if re.search(pattern, text) else 'no'}")
print("--- documented native/ref surface ---")
for name, path in docs.items():
text = path.read_text()
block = re.search(
rf"export interface {name}\s*\{{([\s\S]*?)\n\}}", text
)
body = block.group(1) if block else ""
native = bool(re.search(r"ComponentProps|HTMLAttributes|KbdProps|KbdGroupProps", body))
ref = bool(re.search(r"\bref\b|Ref", body))
print(f"{name}: native-base={'yes' if native else 'no'}, ref-member={'yes' if ref else 'no'}")
print("--- forwarding tests ---")
tests = Path("packages/raystack/components/kbd/__tests__/kbd.test.tsx").read_text()
command_tests = Path("packages/raystack/components/command/__tests__/command.test.tsx").read_text()
for label, text, needles in [
("Kbd", tests, ("forwards arbitrary props", "forwards ref")),
("Command.Shortcut", command_tests, ("forwards props", "forwards ref")),
]:
print(f"{label}: " + ", ".join(f"{n}={'yes' if n in text else 'no'}" for n in needles))
PY
printf '%s\n' '--- type-table and generated-doc configuration ---'
rg -n -C 4 'auto-type-table|fumadocs-typescript|fumadocs-docgen|TypeTable|props\.ts' \
apps/www package.json pnpm-lock.yaml 2>/dev/null | head -260Repository: raystack/apsara Length of output: 17302 Align the documentation prop tables with the published component types. The package already types and forwards native 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| /** Additional CSS class names. */ | ||
| className?: string; | ||
| } | ||
|
|
||
| export interface KbdGroupProps { | ||
| /** The keys in the sequence, plus any plain-text separators between them. */ | ||
| children?: ReactNode; | ||
|
|
||
| /** | ||
| * Visual style variant applied to every key in the group. | ||
| * @defaultValue "solid" | ||
| */ | ||
| variant?: 'solid' | 'ghost'; | ||
|
Comment on lines
+21
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use one variant contract in all API documentation. The runtime treats group and shortcut variants as inherited defaults. Explicit child
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
|
|
||
| /** Additional CSS class names. */ | ||
| className?: string; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add examples of usage in Input