From d679eefeadd632bfad28a3683247307ee054fdb9 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 11 Aug 2026 03:30:10 +0530 Subject: [PATCH 1/7] feat: add Kbd component --- .../src/content/docs/components/kbd/demo.ts | 87 +++++++++++++ .../src/content/docs/components/kbd/index.mdx | 96 ++++++++++++++ .../src/content/docs/components/kbd/props.ts | 17 +++ .../kbd/__tests__/data-slots.test.tsx | 38 ++++++ .../components/kbd/__tests__/kbd.test.tsx | 119 ++++++++++++++++++ packages/raystack/components/kbd/index.tsx | 1 + .../raystack/components/kbd/kbd.module.css | 32 +++++ packages/raystack/components/kbd/kbd.tsx | 32 +++++ packages/raystack/index.tsx | 1 + 9 files changed, 423 insertions(+) create mode 100644 apps/www/src/content/docs/components/kbd/demo.ts create mode 100644 apps/www/src/content/docs/components/kbd/index.mdx create mode 100644 apps/www/src/content/docs/components/kbd/props.ts create mode 100644 packages/raystack/components/kbd/__tests__/data-slots.test.tsx create mode 100644 packages/raystack/components/kbd/__tests__/kbd.test.tsx create mode 100644 packages/raystack/components/kbd/index.tsx create mode 100644 packages/raystack/components/kbd/kbd.module.css create mode 100644 packages/raystack/components/kbd/kbd.tsx diff --git a/apps/www/src/content/docs/components/kbd/demo.ts b/apps/www/src/content/docs/components/kbd/demo.ts new file mode 100644 index 000000000..68dd4874d --- /dev/null +++ b/apps/www/src/content/docs/components/kbd/demo.ts @@ -0,0 +1,87 @@ +'use client'; + +export const preview = { + type: 'code', + code: ` + + K + ` +}; + +export const singleDemo = { + type: 'code', + code: ` + Esc + + + + Tab + ` +}; + +export const groupDemo = { + type: 'code', + code: ` + + + K + + + + + P + + ` +}; + +export const separatorDemo = { + type: 'code', + tabs: [ + { + name: 'Plus', + code: ` + + + + K + ` + }, + { + name: 'Then', + code: ` + G + then + P + ` + } + ] +}; + +export const withTextDemo = { + type: 'code', + code: ` + Press + + + K + + to open the command palette + ` +}; + +export const withTooltipDemo = { + type: 'code', + code: ` + }> + Search + + + + Open search + + + K + + + + ` +}; diff --git a/apps/www/src/content/docs/components/kbd/index.mdx b/apps/www/src/content/docs/components/kbd/index.mdx new file mode 100644 index 000000000..08538bea8 --- /dev/null +++ b/apps/www/src/content/docs/components/kbd/index.mdx @@ -0,0 +1,96 @@ +--- +title: Kbd +description: Displays a keyboard key or a shortcut sequence. +source: packages/raystack/components/kbd +tag: new +--- + +import { + preview, + singleDemo, + groupDemo, + separatorDemo, + withTextDemo, + withTooltipDemo, +} from "./demo.ts"; + + + +## 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"; + +Esc + + + + K + +``` + +## API Reference + +Both parts render a `` element and forward any native attributes (`id`, `title`, `aria-label`, …) to it. + +### Root + +A single keyboard key. Renders a `` element. + + + +### Group + +Spaces a sequence of keys evenly. Also renders a ``: per the HTML spec, a `kbd` nested inside a `kbd` represents an individual key within a larger input, which is exactly what a shortcut sequence is. + + + +### 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 `⌘`. + + + +### Sequences + +Wrap keys in `Kbd.Group` to show a chord. + + + +### 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. + + + +### Inline with text + +Keys sit on the text baseline, so they can be dropped into a sentence. + + + +### In a tooltip + +A common use is surfacing a shortcut alongside the action it triggers. + + + +## Accessibility + +- `Kbd` is presentational and renders the semantic `` element, which screen readers announce as keyboard input. +- Symbol-only keys such as `⌘`, `⇧`, or `↵` are not announced usefully on their own. Add an `aria-label` when the symbol is the only cue: ``. +- Keys are not focusable and carry no interaction. Keep the shortcut wired to a real handler elsewhere — `Kbd` only displays it. +- Text selection is disabled so dragging across a menu row does not highlight the key labels. diff --git a/apps/www/src/content/docs/components/kbd/props.ts b/apps/www/src/content/docs/components/kbd/props.ts new file mode 100644 index 000000000..67fa91b3f --- /dev/null +++ b/apps/www/src/content/docs/components/kbd/props.ts @@ -0,0 +1,17 @@ +import type { ReactNode } from 'react'; + +export interface KbdProps { + /** The key to display, e.g. `⌘`, `Esc`, or `Enter`. */ + children?: ReactNode; + + /** Additional CSS class names. */ + className?: string; +} + +export interface KbdGroupProps { + /** The keys in the sequence, plus any plain-text separators between them. */ + children?: ReactNode; + + /** Additional CSS class names. */ + className?: string; +} diff --git a/packages/raystack/components/kbd/__tests__/data-slots.test.tsx b/packages/raystack/components/kbd/__tests__/data-slots.test.tsx new file mode 100644 index 000000000..577ef4b09 --- /dev/null +++ b/packages/raystack/components/kbd/__tests__/data-slots.test.tsx @@ -0,0 +1,38 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { expectSlots, getAllSlots, getSlot } from '~/test-utils/data-slots'; +import { Kbd } from '../kbd'; + +describe('Kbd data-slot contract', () => { + it('exposes slots for every rendered part', () => { + const { container } = render( + + + K + + ); + expectSlots(container, ['kbd-group', 'kbd']); + }); + + it('marks each key with the same slot name', () => { + const { container } = render( + + + K + + ); + expect(getAllSlots(container, 'kbd')).toHaveLength(2); + }); + + it('drops the group slot when no group is rendered', () => { + const { container } = render(Esc); + expectSlots(container, ['kbd']); + expect(getSlot(container, 'kbd-group')).toBeNull(); + }); + + it('lets callers override the slot name', () => { + const { container } = render(Esc); + expect(getSlot(container, 'custom')).not.toBeNull(); + expect(getSlot(container, 'kbd')).toBeNull(); + }); +}); diff --git a/packages/raystack/components/kbd/__tests__/kbd.test.tsx b/packages/raystack/components/kbd/__tests__/kbd.test.tsx new file mode 100644 index 000000000..fa0f8f401 --- /dev/null +++ b/packages/raystack/components/kbd/__tests__/kbd.test.tsx @@ -0,0 +1,119 @@ +import { render, screen } from '@testing-library/react'; +import { createRef } from 'react'; +import { describe, expect, it } from 'vitest'; +import { Kbd } from '../kbd'; +import styles from '../kbd.module.css'; + +describe('Kbd', () => { + describe('Basic Rendering', () => { + it('renders its children', () => { + render(Ctrl); + expect(screen.getByText('Ctrl')).toBeInTheDocument(); + }); + + it('renders a kbd element', () => { + render(Ctrl); + expect(screen.getByText('Ctrl').tagName).toBe('KBD'); + }); + + it('applies the base class', () => { + render(Ctrl); + expect(screen.getByText('Ctrl')).toHaveClass(styles.kbd); + }); + + it('merges a custom className with the base class', () => { + render(Ctrl); + const kbd = screen.getByText('Ctrl'); + expect(kbd).toHaveClass(styles.kbd); + expect(kbd).toHaveClass('custom'); + }); + + it('forwards arbitrary props to the element', () => { + render(Ctrl); + expect(screen.getByText('Ctrl')).toHaveAttribute( + 'aria-label', + 'Control key' + ); + }); + + it('forwards ref', () => { + const ref = createRef(); + render(Ctrl); + expect(ref.current).toBeInstanceOf(HTMLElement); + expect(ref.current?.tagName).toBe('KBD'); + }); + }); + + describe('Kbd.Group', () => { + it('renders every key it contains', () => { + render( + + + K + + ); + expect(screen.getByText('⌘')).toBeInTheDocument(); + expect(screen.getByText('K')).toBeInTheDocument(); + }); + + it('renders a kbd element so nested keys stay semantic', () => { + const { container } = render( + + K + + ); + const group = container.querySelector(`.${styles['kbd-group']}`); + expect(group?.tagName).toBe('KBD'); + }); + + it('applies the group class, not the key class', () => { + const { container } = render( + + K + + ); + const group = container.querySelector(`.${styles['kbd-group']}`); + expect(group).not.toHaveClass(styles.kbd); + }); + + it('merges a custom className with the group class', () => { + const { container } = render( + + K + + ); + const group = container.querySelector(`.${styles['kbd-group']}`); + expect(group).toHaveClass('custom'); + }); + + it('forwards ref', () => { + const ref = createRef(); + render( + + K + + ); + expect(ref.current?.tagName).toBe('KBD'); + }); + + it('allows plain text separators between keys', () => { + render( + + +K + + ); + expect(screen.getByText('+')).toBeInTheDocument(); + }); + }); + + describe('Composition', () => { + it('exposes Group off the root', () => { + expect(Kbd.Group).toBeDefined(); + }); + + it('sets displayName on both parts', () => { + expect(Kbd.displayName).toBe('Kbd'); + expect(Kbd.Group.displayName).toBe('Kbd.Group'); + }); + }); +}); diff --git a/packages/raystack/components/kbd/index.tsx b/packages/raystack/components/kbd/index.tsx new file mode 100644 index 000000000..fbbeae156 --- /dev/null +++ b/packages/raystack/components/kbd/index.tsx @@ -0,0 +1 @@ +export { Kbd } from './kbd'; diff --git a/packages/raystack/components/kbd/kbd.module.css b/packages/raystack/components/kbd/kbd.module.css new file mode 100644 index 000000000..400847896 --- /dev/null +++ b/packages/raystack/components/kbd/kbd.module.css @@ -0,0 +1,32 @@ +/* normalize.css sets `kbd { font-family: monospace }`, so both parts restore + the body font explicitly rather than relying on inheritance. */ + +.kbd, +.kbd-group { + display: inline-flex; + align-items: center; + color: var(--rs-color-foreground-base-tertiary); + font-family: var(--rs-font-body); + font-size: var(--rs-font-size-mini); + line-height: var(--rs-line-height-mini); + letter-spacing: var(--rs-letter-spacing-mini); +} + +.kbd { + justify-content: center; + box-sizing: border-box; + height: var(--rs-space-6); + /* Square minimum so a narrow "K" reads the same width as a wide "⌘". */ + min-width: var(--rs-space-6); + padding: 0 var(--rs-space-2); + border-radius: var(--rs-radius-1); + background: var(--rs-color-background-neutral-primary); + font-weight: var(--rs-font-weight-medium); + white-space: nowrap; + user-select: none; +} + +/* Spacing container only — the nested keys carry the chip treatment. */ +.kbd-group { + gap: var(--rs-space-2); +} diff --git a/packages/raystack/components/kbd/kbd.tsx b/packages/raystack/components/kbd/kbd.tsx new file mode 100644 index 000000000..dfc60c6b9 --- /dev/null +++ b/packages/raystack/components/kbd/kbd.tsx @@ -0,0 +1,32 @@ +import { cx } from 'class-variance-authority'; +import type { ComponentProps } from 'react'; +import styles from './kbd.module.css'; + +export type KbdProps = ComponentProps<'kbd'>; + +const KbdRoot = ({ className, ...props }: KbdProps) => ( + +); + +KbdRoot.displayName = 'Kbd'; + +export type KbdGroupProps = ComponentProps<'kbd'>; + +/** + * Renders a `` rather than a `
`: per the HTML spec a `kbd` nested + * inside a `kbd` represents an individual key within a larger input, which is + * exactly a shortcut sequence. + */ +const KbdGroup = ({ className, ...props }: KbdGroupProps) => ( + +); + +KbdGroup.displayName = 'Kbd.Group'; + +export const Kbd = Object.assign(KbdRoot, { + Group: KbdGroup +}); diff --git a/packages/raystack/index.tsx b/packages/raystack/index.tsx index 562cbdb2d..c48d4e57e 100644 --- a/packages/raystack/index.tsx +++ b/packages/raystack/index.tsx @@ -97,6 +97,7 @@ export { IconButton } from './components/icon-button'; export { Image } from './components/image'; export { Indicator } from './components/indicator'; export { Input } from './components/input'; +export { Kbd } from './components/kbd'; export { Label } from './components/label'; export { Link } from './components/link'; export { List } from './components/list'; From b5a3b97c63f3f940d51a508ec12a2b31ca788320 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Wed, 12 Aug 2026 17:59:18 +0530 Subject: [PATCH 2/7] feat: add Kbd variants and address review feedback Addresses the review on #886. Component: - Add `solid` (default) and `ghost` variants. `variant` on `Kbd.Group` propagates to its keys via context, with a per-key override. - Add `width: fit-content` so a key no longer stretches in a column-flex or grid parent, and `pointer-events: none` alongside `user-select: none`. - Move typography onto `.kbd` only; `.kbd-group` takes `font: inherit` so separator text picks up the surrounding type instead of the key styling (and still escapes normalize's monospace default for `kbd`). - Drop the explanatory comments. Command: - `Command.Shortcut` is now an alias of `Kbd.Group` + `Kbd` defaulting to `ghost`, forwarding all props. Whitespace splitting and the `command-shortcut` / `command-shortcut-key` slots are preserved, and element children pass through without a second key wrapper. - Remove the now-unused shortcut typography. Accessibility: - `ghost` uses `foreground-base-secondary`. `tertiary` measured 3.33-4.46:1 across the surfaces Kbd is documented on, below AA's 4.5 for 11px text and worst on a hovered row; `secondary` clears it everywhere at >=5.19:1. - Correct the docs claim about screen readers: `kbd` maps to no ARIA role and no accessible object per HTML-AAM. - Label symbol-only keys in the examples. Docs: - Add a playground and a Variants section, reword the description and the Group summary, and make the "Inline with text" example actually inline. - Add an Input example, using a single key since the trailing slot is sized for an icon and clips a multi-key group. - Document `variant` on `Command.Shortcut`. Co-Authored-By: Claude Opus 5 (1M context) --- .../content/docs/components/command/index.mdx | 2 +- .../content/docs/components/command/props.ts | 12 +++ .../src/content/docs/components/kbd/demo.ts | 80 +++++++++++++------ .../src/content/docs/components/kbd/index.mdx | 34 +++++--- .../src/content/docs/components/kbd/props.ts | 12 +++ .../command/__tests__/command.test.tsx | 55 +++++++++++++ .../components/command/command-misc.tsx | 29 ++++--- .../components/command/command.module.css | 14 ---- .../components/kbd/__tests__/kbd.test.tsx | 55 +++++++++++++ packages/raystack/components/kbd/index.tsx | 2 +- .../raystack/components/kbd/kbd.module.css | 30 ++++--- packages/raystack/components/kbd/kbd.tsx | 61 +++++++++----- 12 files changed, 293 insertions(+), 93 deletions(-) diff --git a/apps/www/src/content/docs/components/command/index.mdx b/apps/www/src/content/docs/components/command/index.mdx index f3b03b5f8..1f0527ac9 100644 --- a/apps/www/src/content/docs/components/command/index.mdx +++ b/apps/www/src/content/docs/components/command/index.mdx @@ -97,7 +97,7 @@ Visual divider between groups. The separator is hidden automatically while the u ### Shortcut -A `` element for keyboard hints. Typically passed as `trailingIcon` on `Command.Item`. +Keyboard hints for an item, typically passed as `trailingIcon` on `Command.Item`. Built on [`Kbd`](/docs/components/kbd): it renders a `Kbd.Group` of `ghost` keys and forwards every prop. diff --git a/apps/www/src/content/docs/components/command/props.ts b/apps/www/src/content/docs/components/command/props.ts index 5b1b54945..906c6580b 100644 --- a/apps/www/src/content/docs/components/command/props.ts +++ b/apps/www/src/content/docs/components/command/props.ts @@ -132,6 +132,18 @@ export interface CommandSeparatorProps { } export interface CommandShortcutProps { + /** + * The keys to display. A whitespace-separated string is split into one key + * per token, so `"⌘ K"` renders two keys. + */ + children?: React.ReactNode; + + /** + * Visual style variant, applied to every key in the shortcut. + * @defaultValue "ghost" + */ + variant?: 'solid' | 'ghost'; + /** Additional CSS class names. */ className?: string; } diff --git a/apps/www/src/content/docs/components/kbd/demo.ts b/apps/www/src/content/docs/components/kbd/demo.ts index 68dd4874d..c5a6f6dca 100644 --- a/apps/www/src/content/docs/components/kbd/demo.ts +++ b/apps/www/src/content/docs/components/kbd/demo.ts @@ -1,34 +1,65 @@ 'use client'; -export const preview = { - type: 'code', - code: ` - - K - ` +import type { ComponentPropsType } from '@/components/demo/types'; +import { getPropsString } from '@/lib/utils'; + +export const getCode = (props: ComponentPropsType) => { + const { children, ...rest } = props; + + return `${children}`; +}; + +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: ` Esc - - - + + + Tab ` }; +export const variantDemo = { + type: 'code', + code: ` + + + K + + + + K + + ` +}; + export const groupDemo = { type: 'code', code: ` - + K - - + + P ` @@ -40,7 +71,7 @@ export const separatorDemo = { { name: 'Plus', code: ` - + + K ` @@ -58,14 +89,17 @@ export const separatorDemo = { export const withTextDemo = { type: 'code', - code: ` - Press - - - K - - to open the command palette - ` + code: ` + Press K to open the command palette. + ` +}; + +export const withInputDemo = { + type: 'code', + code: `⌘K} + />` }; export const withTooltipDemo = { @@ -77,8 +111,8 @@ export const withTooltipDemo = { Open search - - + + K diff --git a/apps/www/src/content/docs/components/kbd/index.mdx b/apps/www/src/content/docs/components/kbd/index.mdx index 08538bea8..8607491e8 100644 --- a/apps/www/src/content/docs/components/kbd/index.mdx +++ b/apps/www/src/content/docs/components/kbd/index.mdx @@ -1,20 +1,22 @@ --- title: Kbd -description: Displays a keyboard key or a shortcut sequence. +description: A component for displaying keyboard keys and shortcuts. source: packages/raystack/components/kbd tag: new --- import { - preview, + playground, singleDemo, + variantDemo, groupDemo, separatorDemo, withTextDemo, + withInputDemo, withTooltipDemo, } from "./demo.ts"; - + ## Anatomy @@ -43,7 +45,7 @@ A single keyboard key. Renders a `` element. ### Group -Spaces a sequence of keys evenly. Also renders a ``: per the HTML spec, a `kbd` nested inside a `kbd` represents an individual key within a larger input, which is exactly what a shortcut sequence is. +Groups multiple keyboard keys for key combinations. @@ -64,24 +66,36 @@ Use `Kbd` on its own for a one-key hint. Keys share a minimum width so a narrow +### 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. + + + ### Sequences -Wrap keys in `Kbd.Group` to show a chord. +Wrap keys in `Kbd.Group` to show a chord. Setting `variant` on the group applies it to every key inside. ### 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. +`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. ### Inline with text -Keys sit on the text baseline, so they can be dropped into a sentence. +Keys sit on the text baseline, so they can be dropped straight into a sentence. +### 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. + + + ### In a tooltip A common use is surfacing a shortcut alongside the action it triggers. @@ -90,7 +104,7 @@ A common use is surfacing a shortcut alongside the action it triggers. ## Accessibility -- `Kbd` is presentational and renders the semantic `` element, which screen readers announce as keyboard input. -- Symbol-only keys such as `⌘`, `⇧`, or `↵` are not announced usefully on their own. Add an `aria-label` when the symbol is the only cue: ``. +- `Kbd` is presentational and renders the semantic `` 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: ``. - Keys are not focusable and carry no interaction. Keep the shortcut wired to a real handler elsewhere — `Kbd` only displays it. -- Text selection is disabled so dragging across a menu row does not highlight the key labels. +- Keys ignore pointer events and text selection, so clicking or dragging across a menu row does not highlight the key labels. diff --git a/apps/www/src/content/docs/components/kbd/props.ts b/apps/www/src/content/docs/components/kbd/props.ts index 67fa91b3f..311b8701a 100644 --- a/apps/www/src/content/docs/components/kbd/props.ts +++ b/apps/www/src/content/docs/components/kbd/props.ts @@ -4,6 +4,12 @@ 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'; + /** Additional CSS class names. */ className?: string; } @@ -12,6 +18,12 @@ 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'; + /** Additional CSS class names. */ className?: string; } diff --git a/packages/raystack/components/command/__tests__/command.test.tsx b/packages/raystack/components/command/__tests__/command.test.tsx index 2cdefaf0d..1b298f1c3 100644 --- a/packages/raystack/components/command/__tests__/command.test.tsx +++ b/packages/raystack/components/command/__tests__/command.test.tsx @@ -2,6 +2,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { describe, expect, it, vi } from 'vitest'; +import { Kbd } from '../../kbd'; +import kbdStyles from '../../kbd/kbd.module.css'; import { Command } from '../command'; import styles from '../command.module.css'; @@ -315,4 +317,57 @@ describe('Command', () => { }); }); }); + + describe('Command.Shortcut', () => { + it('splits a string of keys into individual keys', () => { + render(⌘ K); + expect(screen.getByText('⌘')).toBeInTheDocument(); + expect(screen.getByText('K')).toBeInTheDocument(); + }); + + it('renders each key through Kbd', () => { + render(⌘ K); + const key = screen.getByText('⌘'); + expect(key.tagName).toBe('KBD'); + expect(key).toHaveClass(kbdStyles['kbd']); + }); + + it('defaults its keys to the ghost variant', () => { + render(⌘ K); + expect(screen.getByText('⌘')).toHaveClass(kbdStyles['kbd-ghost']); + }); + + it('allows the variant to be overridden', () => { + render(⌘ K); + expect(screen.getByText('⌘')).toHaveClass(kbdStyles['kbd-solid']); + }); + + it('forwards props and merges className onto the group', () => { + const { container } = render( + + ⌘ K + + ); + const group = container.querySelector('[data-slot="command-shortcut"]'); + expect(group).toHaveClass('custom'); + expect(group).toHaveClass(styles.shortcut); + expect(group).toHaveAttribute('aria-label', 'Command K'); + }); + + it('forwards ref', () => { + const ref = React.createRef(); + render(⌘ K); + expect(ref.current?.tagName).toBe('KBD'); + }); + + it('does not double-wrap element children in a second key', () => { + const { container } = render( + + + + ); + const keys = container.querySelectorAll(`.${kbdStyles['kbd']}`); + expect(keys).toHaveLength(1); + }); + }); }); diff --git a/packages/raystack/components/command/command-misc.tsx b/packages/raystack/components/command/command-misc.tsx index a1563995a..8bcefe9d8 100644 --- a/packages/raystack/components/command/command-misc.tsx +++ b/packages/raystack/components/command/command-misc.tsx @@ -2,7 +2,8 @@ import { Autocomplete as AutocompletePrimitive } from '@base-ui/react/autocomplete'; import { cx } from 'class-variance-authority'; -import { type ComponentProps } from 'react'; +import { Fragment, isValidElement } from 'react'; +import { Kbd, type KbdGroupProps } from '../kbd'; import styles from './command.module.css'; import { useCommandContext } from './command-root'; @@ -63,11 +64,12 @@ export const CommandSeparator = ({ }; CommandSeparator.displayName = 'Command.Separator'; -export type CommandShortcutProps = ComponentProps<'span'>; +export type CommandShortcutProps = KbdGroupProps; export const CommandShortcut = ({ className, children, + variant = 'ghost', ...props }: CommandShortcutProps) => { const keys = @@ -78,21 +80,22 @@ export const CommandShortcut = ({ : [children]; return ( - - {keys.map((key, index) => ( - - {key} - - ))} - + {keys.map((key, index) => + isValidElement(key) ? ( + {key} + ) : ( + + {key} + + ) + )} + ); }; CommandShortcut.displayName = 'Command.Shortcut'; diff --git a/packages/raystack/components/command/command.module.css b/packages/raystack/components/command/command.module.css index 0cbdd837e..0eaa63891 100644 --- a/packages/raystack/components/command/command.module.css +++ b/packages/raystack/components/command/command.module.css @@ -103,22 +103,8 @@ } .shortcut { - display: inline-flex; - align-items: center; justify-content: flex-end; - gap: var(--rs-space-1); white-space: nowrap; - font-family: var(--rs-font-body); - font-weight: var(--rs-font-weight-regular); - font-size: var(--rs-font-size-micro); - line-height: var(--rs-line-height-micro); - letter-spacing: var(--rs-letter-spacing-micro); - color: var(--rs-color-foreground-base-tertiary); -} - -.shortcutKey { - font: inherit; - color: inherit; } .empty { diff --git a/packages/raystack/components/kbd/__tests__/kbd.test.tsx b/packages/raystack/components/kbd/__tests__/kbd.test.tsx index fa0f8f401..79a353504 100644 --- a/packages/raystack/components/kbd/__tests__/kbd.test.tsx +++ b/packages/raystack/components/kbd/__tests__/kbd.test.tsx @@ -44,6 +44,61 @@ describe('Kbd', () => { }); }); + describe('Variants', () => { + it('applies the solid variant by default', () => { + render(Ctrl); + expect(screen.getByText('Ctrl')).toHaveClass(styles['kbd-solid']); + }); + + it('applies the ghost variant when requested', () => { + render(Ctrl); + const kbd = screen.getByText('Ctrl'); + expect(kbd).toHaveClass(styles['kbd-ghost']); + expect(kbd).not.toHaveClass(styles['kbd-solid']); + }); + + it('inherits the variant from a parent group', () => { + render( + + + K + + ); + expect(screen.getByText('⌘')).toHaveClass(styles['kbd-ghost']); + expect(screen.getByText('K')).toHaveClass(styles['kbd-ghost']); + }); + + it('lets a key override the variant inherited from its group', () => { + render( + + + K + + ); + expect(screen.getByText('⌘')).toHaveClass(styles['kbd-solid']); + expect(screen.getByText('K')).toHaveClass(styles['kbd-ghost']); + }); + + it('falls back to solid for keys in a group with no variant', () => { + render( + + K + + ); + expect(screen.getByText('K')).toHaveClass(styles['kbd-solid']); + }); + + it('does not put a key variant class on the group', () => { + const { container } = render( + + K + + ); + const group = container.querySelector(`.${styles['kbd-group']}`); + expect(group).not.toHaveClass(styles['kbd-ghost']); + }); + }); + describe('Kbd.Group', () => { it('renders every key it contains', () => { render( diff --git a/packages/raystack/components/kbd/index.tsx b/packages/raystack/components/kbd/index.tsx index fbbeae156..a170b82e2 100644 --- a/packages/raystack/components/kbd/index.tsx +++ b/packages/raystack/components/kbd/index.tsx @@ -1 +1 @@ -export { Kbd } from './kbd'; +export { Kbd, type KbdGroupProps, type KbdProps } from './kbd'; diff --git a/packages/raystack/components/kbd/kbd.module.css b/packages/raystack/components/kbd/kbd.module.css index 400847896..b899ad6f9 100644 --- a/packages/raystack/components/kbd/kbd.module.css +++ b/packages/raystack/components/kbd/kbd.module.css @@ -1,32 +1,38 @@ -/* normalize.css sets `kbd { font-family: monospace }`, so both parts restore - the body font explicitly rather than relying on inheritance. */ - .kbd, .kbd-group { display: inline-flex; align-items: center; - color: var(--rs-color-foreground-base-tertiary); - font-family: var(--rs-font-body); - font-size: var(--rs-font-size-mini); - line-height: var(--rs-line-height-mini); - letter-spacing: var(--rs-letter-spacing-mini); + width: fit-content; + pointer-events: none; + user-select: none; } .kbd { justify-content: center; box-sizing: border-box; height: var(--rs-space-6); - /* Square minimum so a narrow "K" reads the same width as a wide "⌘". */ min-width: var(--rs-space-6); padding: 0 var(--rs-space-2); border-radius: var(--rs-radius-1); - background: var(--rs-color-background-neutral-primary); + font-family: var(--rs-font-body); + font-size: var(--rs-font-size-mini); font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-mini); + letter-spacing: var(--rs-letter-spacing-mini); white-space: nowrap; - user-select: none; } -/* Spacing container only — the nested keys carry the chip treatment. */ +.kbd-solid { + background: var(--rs-color-background-neutral-primary); + color: var(--rs-color-foreground-base-secondary); +} + +.kbd-ghost { + background: transparent; + color: var(--rs-color-foreground-base-secondary); +} + .kbd-group { gap: var(--rs-space-2); + font: inherit; } diff --git a/packages/raystack/components/kbd/kbd.tsx b/packages/raystack/components/kbd/kbd.tsx index dfc60c6b9..ad9a6ccbc 100644 --- a/packages/raystack/components/kbd/kbd.tsx +++ b/packages/raystack/components/kbd/kbd.tsx @@ -1,28 +1,51 @@ -import { cx } from 'class-variance-authority'; -import type { ComponentProps } from 'react'; +'use client'; + +import { cva, cx, type VariantProps } from 'class-variance-authority'; +import { type ComponentProps, createContext, useContext } from 'react'; import styles from './kbd.module.css'; -export type KbdProps = ComponentProps<'kbd'>; +const kbd = cva(styles['kbd'], { + variants: { + variant: { + solid: styles['kbd-solid'], + ghost: styles['kbd-ghost'] + } + }, + defaultVariants: { + variant: 'solid' + } +}); + +type KbdVariant = NonNullable['variant']>; -const KbdRoot = ({ className, ...props }: KbdProps) => ( - -); +const KbdGroupContext = createContext(undefined); + +export type KbdProps = ComponentProps<'kbd'> & VariantProps; + +const KbdRoot = ({ className, variant, ...props }: KbdProps) => { + const groupVariant = useContext(KbdGroupContext); + + return ( + + ); +}; KbdRoot.displayName = 'Kbd'; -export type KbdGroupProps = ComponentProps<'kbd'>; - -/** - * Renders a `` rather than a `
`: per the HTML spec a `kbd` nested - * inside a `kbd` represents an individual key within a larger input, which is - * exactly a shortcut sequence. - */ -const KbdGroup = ({ className, ...props }: KbdGroupProps) => ( - +export type KbdGroupProps = ComponentProps<'kbd'> & VariantProps; + +const KbdGroup = ({ className, variant, ...props }: KbdGroupProps) => ( + + + ); KbdGroup.displayName = 'Kbd.Group'; From 5a6df3150d54c1638b71f82dd85aff212f4361f6 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Mon, 17 Aug 2026 13:43:11 +0530 Subject: [PATCH 3/7] docs: clarify that a key can override its group's variant The group and shortcut variants are inherited defaults, not applied unconditionally: an explicit `variant` on a child `Kbd` wins, and a `Kbd` passed to `Command.Shortcut` as an element child keeps its own variant. Reword the four places that described the variant as applying to every key, and state that `ghost` is `Command.Shortcut`'s default rather than its only option. Co-Authored-By: Claude Opus 5 (1M context) --- apps/www/src/content/docs/components/command/index.mdx | 2 +- apps/www/src/content/docs/components/command/props.ts | 3 ++- apps/www/src/content/docs/components/kbd/index.mdx | 2 +- apps/www/src/content/docs/components/kbd/props.ts | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/www/src/content/docs/components/command/index.mdx b/apps/www/src/content/docs/components/command/index.mdx index 1f0527ac9..d36382690 100644 --- a/apps/www/src/content/docs/components/command/index.mdx +++ b/apps/www/src/content/docs/components/command/index.mdx @@ -97,7 +97,7 @@ Visual divider between groups. The separator is hidden automatically while the u ### Shortcut -Keyboard hints for an item, typically passed as `trailingIcon` on `Command.Item`. Built on [`Kbd`](/docs/components/kbd): it renders a `Kbd.Group` of `ghost` keys and forwards every prop. +Keyboard hints for an item, typically passed as `trailingIcon` on `Command.Item`. Built on [`Kbd`](/docs/components/kbd): it renders a `Kbd.Group` whose keys default to the `ghost` variant, and forwards every prop. diff --git a/apps/www/src/content/docs/components/command/props.ts b/apps/www/src/content/docs/components/command/props.ts index 906c6580b..8b6968b06 100644 --- a/apps/www/src/content/docs/components/command/props.ts +++ b/apps/www/src/content/docs/components/command/props.ts @@ -139,7 +139,8 @@ export interface CommandShortcutProps { children?: React.ReactNode; /** - * Visual style variant, applied to every key in the shortcut. + * Visual style variant inherited by the keys of the shortcut. A `Kbd` + * passed as a child keeps its own `variant`. * @defaultValue "ghost" */ variant?: 'solid' | 'ghost'; diff --git a/apps/www/src/content/docs/components/kbd/index.mdx b/apps/www/src/content/docs/components/kbd/index.mdx index 8607491e8..a07f1a74f 100644 --- a/apps/www/src/content/docs/components/kbd/index.mdx +++ b/apps/www/src/content/docs/components/kbd/index.mdx @@ -74,7 +74,7 @@ Use `Kbd` on its own for a one-key hint. Keys share a minimum width so a narrow ### Sequences -Wrap keys in `Kbd.Group` to show a chord. Setting `variant` on the group applies it to every key inside. +Wrap keys in `Kbd.Group` to show a chord. Setting `variant` on the group sets the variant for the keys inside, and an individual `Kbd` can override it. diff --git a/apps/www/src/content/docs/components/kbd/props.ts b/apps/www/src/content/docs/components/kbd/props.ts index 311b8701a..8fa99e84b 100644 --- a/apps/www/src/content/docs/components/kbd/props.ts +++ b/apps/www/src/content/docs/components/kbd/props.ts @@ -19,7 +19,8 @@ export interface KbdGroupProps { children?: ReactNode; /** - * Visual style variant applied to every key in the group. + * Visual style variant inherited by every key in the group. A key's own + * `variant` takes precedence over it. * @defaultValue "solid" */ variant?: 'solid' | 'ghost'; From fc9656356953c248283969d57121485fedaf598b Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 18 Aug 2026 13:04:21 +0530 Subject: [PATCH 4/7] refactor: drop Kbd group context and Command.Shortcut Address review feedback on #886. - Kbd.Group no longer uses React context to pass its variant down. The group marks itself with a variant class and kbd.module.css resolves inheritance via :where(), keeping specificity low enough that a key's own variant still wins (same pattern as radio.module.css). - Solid colors move onto the base .kbd so an unset key stays solid without a defaultVariant fighting group inheritance. - Ghost keys use foreground-base-tertiary, as requested in review. - Remove Command.Shortcut along with its string-splitting loop. Items take a Kbd or Kbd.Group as trailingIcon instead; docs, demos, slot tables, and tests follow. --- .../content/docs/components/command/demo.ts | 37 ++++++++-- .../content/docs/components/command/index.mdx | 10 +-- .../content/docs/components/command/props.ts | 20 +----- .../command/__tests__/command.test.tsx | 67 ++++++------------- .../command/__tests__/data-slots.test.tsx | 10 ++- .../components/command/command-item.tsx | 2 +- .../components/command/command-misc.tsx | 38 ----------- .../components/command/command.module.css | 5 -- .../raystack/components/command/command.tsx | 8 +-- .../components/kbd/__tests__/kbd.test.tsx | 35 ++++++---- .../raystack/components/kbd/kbd.module.css | 10 ++- packages/raystack/components/kbd/kbd.tsx | 47 ++++++------- 12 files changed, 113 insertions(+), 176 deletions(-) diff --git a/apps/www/src/content/docs/components/command/demo.ts b/apps/www/src/content/docs/components/command/demo.ts index 2c60ec0f0..fc5ba8ff1 100644 --- a/apps/www/src/content/docs/components/command/demo.ts +++ b/apps/www/src/content/docs/components/command/demo.ts @@ -27,14 +27,26 @@ export const preview = { Actions } - trailingIcon={⌘ ⇧ A} + trailingIcon={ + + + + A + + } onClick={() => setOpen(false)} > Create AOI... } - trailingIcon={⌘ ⇧ W} + trailingIcon={ + + + + W + + } onClick={() => setOpen(false)} disabled > @@ -137,19 +149,34 @@ export const shortcutDemo = { Suggestions ⌘ P} + trailingIcon={ + + + P + + } onClick={() => setOpen(false)} > Profile ⌘ B} + trailingIcon={ + + + B + + } onClick={() => setOpen(false)} > Billing ⌘ S} + trailingIcon={ + + + S + + } onClick={() => setOpen(false)} > Settings diff --git a/apps/www/src/content/docs/components/command/index.mdx b/apps/www/src/content/docs/components/command/index.mdx index d36382690..22a5e21ef 100644 --- a/apps/www/src/content/docs/components/command/index.mdx +++ b/apps/www/src/content/docs/components/command/index.mdx @@ -73,7 +73,7 @@ Rendered when no items are visible (all filtered out, or the list is empty). Aut ### Item -A selectable entry. When `value` is omitted and `children` is a string, the string is used as the value. `onClick` fires on pointer click **and** on keyboard `Enter` when the item is highlighted. +A selectable entry. When `value` is omitted and `children` is a string, the string is used as the value. `onClick` fires on pointer click **and** on keyboard `Enter` when the item is highlighted. Pass a [`Kbd`](/docs/components/kbd) as `trailingIcon` to show a keyboard hint — use the `ghost` variant so the keys sit on the item's own background. @@ -95,12 +95,6 @@ Visual divider between groups. The separator is hidden automatically while the u -### Shortcut - -Keyboard hints for an item, typically passed as `trailingIcon` on `Command.Item`. Built on [`Kbd`](/docs/components/kbd): it renders a `Kbd.Group` whose keys default to the `ghost` variant, and forwards every prop. - - - ### Dialog `Command.Dialog` is an alias for the Base UI Dialog root. Pair it with `Command.DialogTrigger` and `Command.DialogContent` to render the command menu in a centered modal. @@ -131,8 +125,6 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test | `command-item-label` | The item's label text | | `command-item-trailing-icon` | Trailing icon inside an item (when provided) | | `command-separator` | `Command.Separator` | -| `command-shortcut` | `Command.Shortcut` wrapper | -| `command-shortcut-key` | Each `` key inside a shortcut | | `command-dialog-trigger` | `Command.DialogTrigger` | | `command-dialog-viewport` | Viewport wrapper for the dialog popup | | `command-dialog-content` | `Command.DialogContent` popup | diff --git a/apps/www/src/content/docs/components/command/props.ts b/apps/www/src/content/docs/components/command/props.ts index 8b6968b06..1b8fc9751 100644 --- a/apps/www/src/content/docs/components/command/props.ts +++ b/apps/www/src/content/docs/components/command/props.ts @@ -103,7 +103,7 @@ export interface CommandItemProps { /** Icon rendered before the item label. */ leadingIcon?: React.ReactNode; - /** Node rendered after the item label (e.g. `Command.Shortcut` or an icon). */ + /** Node rendered after the item label (e.g. a `Kbd` shortcut hint or an icon). */ trailingIcon?: React.ReactNode; /** @@ -131,24 +131,6 @@ export interface CommandSeparatorProps { className?: string; } -export interface CommandShortcutProps { - /** - * The keys to display. A whitespace-separated string is split into one key - * per token, so `"⌘ K"` renders two keys. - */ - children?: React.ReactNode; - - /** - * Visual style variant inherited by the keys of the shortcut. A `Kbd` - * passed as a child keeps its own `variant`. - * @defaultValue "ghost" - */ - variant?: 'solid' | 'ghost'; - - /** Additional CSS class names. */ - className?: string; -} - export interface CommandDialogProps { /** Controlled open state. */ open?: boolean; diff --git a/packages/raystack/components/command/__tests__/command.test.tsx b/packages/raystack/components/command/__tests__/command.test.tsx index 1b298f1c3..8075d7c0e 100644 --- a/packages/raystack/components/command/__tests__/command.test.tsx +++ b/packages/raystack/components/command/__tests__/command.test.tsx @@ -318,56 +318,29 @@ describe('Command', () => { }); }); - describe('Command.Shortcut', () => { - it('splits a string of keys into individual keys', () => { - render(⌘ K); - expect(screen.getByText('⌘')).toBeInTheDocument(); - expect(screen.getByText('K')).toBeInTheDocument(); - }); - - it('renders each key through Kbd', () => { - render(⌘ K); - const key = screen.getByText('⌘'); - expect(key.tagName).toBe('KBD'); - expect(key).toHaveClass(kbdStyles['kbd']); - }); - - it('defaults its keys to the ghost variant', () => { - render(⌘ K); - expect(screen.getByText('⌘')).toHaveClass(kbdStyles['kbd-ghost']); - }); - - it('allows the variant to be overridden', () => { - render(⌘ K); - expect(screen.getByText('⌘')).toHaveClass(kbdStyles['kbd-solid']); - }); - - it('forwards props and merges className onto the group', () => { + describe('Shortcut hints', () => { + it('renders a Kbd.Group passed as trailingIcon', () => { const { container } = render( - - ⌘ K - + + + + + K + + } + > + Search + + + ); - const group = container.querySelector('[data-slot="command-shortcut"]'); - expect(group).toHaveClass('custom'); - expect(group).toHaveClass(styles.shortcut); - expect(group).toHaveAttribute('aria-label', 'Command K'); - }); - - it('forwards ref', () => { - const ref = React.createRef(); - render(⌘ K); - expect(ref.current?.tagName).toBe('KBD'); - }); - - it('does not double-wrap element children in a second key', () => { - const { container } = render( - - - + expect(screen.getByText('⌘')).toBeInTheDocument(); + expect(screen.getByText('K')).toBeInTheDocument(); + expect(container.querySelectorAll(`.${kbdStyles['kbd']}`)).toHaveLength( + 2 ); - const keys = container.querySelectorAll(`.${kbdStyles['kbd']}`); - expect(keys).toHaveLength(1); }); }); }); diff --git a/packages/raystack/components/command/__tests__/data-slots.test.tsx b/packages/raystack/components/command/__tests__/data-slots.test.tsx index 8bb13ddb2..5b2b1c4cf 100644 --- a/packages/raystack/components/command/__tests__/data-slots.test.tsx +++ b/packages/raystack/components/command/__tests__/data-slots.test.tsx @@ -1,6 +1,7 @@ import { render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { expectSlots, getSlot } from '~/test-utils/data-slots'; +import { Kbd } from '../../kbd'; import { Command } from '../command'; // Mock scrollIntoView for test environment @@ -18,7 +19,12 @@ const BasicCommand = () => ( Suggestions } - trailingIcon={⌘ K} + trailingIcon={ + + + K + + } > Calendar @@ -44,8 +50,6 @@ describe('Command data-slot contract', () => { 'command-item-leading-icon', 'command-item-label', 'command-item-trailing-icon', - 'command-shortcut', - 'command-shortcut-key', 'command-separator' ]); }); diff --git a/packages/raystack/components/command/command-item.tsx b/packages/raystack/components/command/command-item.tsx index ffd5609e5..3990d8edb 100644 --- a/packages/raystack/components/command/command-item.tsx +++ b/packages/raystack/components/command/command-item.tsx @@ -10,7 +10,7 @@ import { useCommandContext } from './command-root'; export interface CommandItemProps extends AutocompletePrimitive.Item.Props { /** Icon rendered at the start of the item. */ leadingIcon?: ReactNode; - /** Icon rendered at the end of the item (e.g. `Command.Shortcut`). */ + /** Icon rendered at the end of the item (e.g. a `Kbd` shortcut hint). */ trailingIcon?: ReactNode; } diff --git a/packages/raystack/components/command/command-misc.tsx b/packages/raystack/components/command/command-misc.tsx index 8bcefe9d8..a4e9f3345 100644 --- a/packages/raystack/components/command/command-misc.tsx +++ b/packages/raystack/components/command/command-misc.tsx @@ -2,8 +2,6 @@ import { Autocomplete as AutocompletePrimitive } from '@base-ui/react/autocomplete'; import { cx } from 'class-variance-authority'; -import { Fragment, isValidElement } from 'react'; -import { Kbd, type KbdGroupProps } from '../kbd'; import styles from './command.module.css'; import { useCommandContext } from './command-root'; @@ -63,39 +61,3 @@ export const CommandSeparator = ({ ); }; CommandSeparator.displayName = 'Command.Separator'; - -export type CommandShortcutProps = KbdGroupProps; - -export const CommandShortcut = ({ - className, - children, - variant = 'ghost', - ...props -}: CommandShortcutProps) => { - const keys = - typeof children === 'string' - ? children.trim().split(/\s+/).filter(Boolean) - : Array.isArray(children) - ? children - : [children]; - - return ( - - {keys.map((key, index) => - isValidElement(key) ? ( - {key} - ) : ( - - {key} - - ) - )} - - ); -}; -CommandShortcut.displayName = 'Command.Shortcut'; diff --git a/packages/raystack/components/command/command.module.css b/packages/raystack/components/command/command.module.css index 0eaa63891..080dbbc3d 100644 --- a/packages/raystack/components/command/command.module.css +++ b/packages/raystack/components/command/command.module.css @@ -102,11 +102,6 @@ height: var(--rs-space-5); } -.shortcut { - justify-content: flex-end; - white-space: nowrap; -} - .empty { padding: var(--rs-space-7) var(--rs-space-3); text-align: center; diff --git a/packages/raystack/components/command/command.tsx b/packages/raystack/components/command/command.tsx index ac01d9e46..a3e197db4 100644 --- a/packages/raystack/components/command/command.tsx +++ b/packages/raystack/components/command/command.tsx @@ -7,12 +7,7 @@ import { import { CommandEmpty } from './command-empty'; import { CommandInput } from './command-input'; import { CommandItem } from './command-item'; -import { - CommandGroup, - CommandLabel, - CommandSeparator, - CommandShortcut -} from './command-misc'; +import { CommandGroup, CommandLabel, CommandSeparator } from './command-misc'; import { CommandRoot } from './command-root'; export const Command = Object.assign(CommandRoot, { @@ -23,7 +18,6 @@ export const Command = Object.assign(CommandRoot, { Group: CommandGroup, Label: CommandLabel, Separator: CommandSeparator, - Shortcut: CommandShortcut, Dialog: CommandDialog, DialogTrigger: CommandDialogTrigger, DialogContent: CommandDialogContent diff --git a/packages/raystack/components/kbd/__tests__/kbd.test.tsx b/packages/raystack/components/kbd/__tests__/kbd.test.tsx index 79a353504..65619b3e0 100644 --- a/packages/raystack/components/kbd/__tests__/kbd.test.tsx +++ b/packages/raystack/components/kbd/__tests__/kbd.test.tsx @@ -45,27 +45,36 @@ describe('Kbd', () => { }); describe('Variants', () => { - it('applies the solid variant by default', () => { - render(Ctrl); + it('carries the solid styling on the base class by default', () => { + const kbd = render(Ctrl).getByText('Ctrl'); + expect(kbd).toHaveClass(styles.kbd); + expect(kbd).not.toHaveClass(styles['kbd-solid']); + expect(kbd).not.toHaveClass(styles['kbd-ghost']); + }); + + it('applies the solid variant when requested', () => { + render(Ctrl); expect(screen.getByText('Ctrl')).toHaveClass(styles['kbd-solid']); }); it('applies the ghost variant when requested', () => { - render(Ctrl); - const kbd = screen.getByText('Ctrl'); + const kbd = render(Ctrl).getByText('Ctrl'); expect(kbd).toHaveClass(styles['kbd-ghost']); expect(kbd).not.toHaveClass(styles['kbd-solid']); }); - it('inherits the variant from a parent group', () => { - render( + it('marks the group so its keys inherit the variant in CSS', () => { + const { container } = render( K ); - expect(screen.getByText('⌘')).toHaveClass(styles['kbd-ghost']); - expect(screen.getByText('K')).toHaveClass(styles['kbd-ghost']); + const group = container.querySelector(`.${styles['kbd-group']}`); + expect(group).toHaveClass(styles['kbd-group-ghost']); + // Keys carry no variant class of their own, so the group rule applies. + expect(screen.getByText('⌘')).not.toHaveClass(styles['kbd-solid']); + expect(screen.getByText('K')).not.toHaveClass(styles['kbd-solid']); }); it('lets a key override the variant inherited from its group', () => { @@ -76,16 +85,18 @@ describe('Kbd', () => { ); expect(screen.getByText('⌘')).toHaveClass(styles['kbd-solid']); - expect(screen.getByText('K')).toHaveClass(styles['kbd-ghost']); + expect(screen.getByText('K')).not.toHaveClass(styles['kbd-solid']); }); - it('falls back to solid for keys in a group with no variant', () => { - render( + it('leaves a group with no variant unmarked', () => { + const { container } = render( K ); - expect(screen.getByText('K')).toHaveClass(styles['kbd-solid']); + const group = container.querySelector(`.${styles['kbd-group']}`); + expect(group).not.toHaveClass(styles['kbd-group-ghost']); + expect(group).not.toHaveClass(styles['kbd-group-solid']); }); it('does not put a key variant class on the group', () => { diff --git a/packages/raystack/components/kbd/kbd.module.css b/packages/raystack/components/kbd/kbd.module.css index b899ad6f9..da07dfdd2 100644 --- a/packages/raystack/components/kbd/kbd.module.css +++ b/packages/raystack/components/kbd/kbd.module.css @@ -20,16 +20,20 @@ line-height: var(--rs-line-height-mini); letter-spacing: var(--rs-letter-spacing-mini); white-space: nowrap; + background: var(--rs-color-background-neutral-primary); + color: var(--rs-color-foreground-base-secondary); } -.kbd-solid { +:where(.kbd-group-solid) .kbd, +.kbd.kbd-solid { background: var(--rs-color-background-neutral-primary); color: var(--rs-color-foreground-base-secondary); } -.kbd-ghost { +:where(.kbd-group-ghost) .kbd, +.kbd.kbd-ghost { background: transparent; - color: var(--rs-color-foreground-base-secondary); + color: var(--rs-color-foreground-base-tertiary); } .kbd-group { diff --git a/packages/raystack/components/kbd/kbd.tsx b/packages/raystack/components/kbd/kbd.tsx index ad9a6ccbc..9f4109bef 100644 --- a/packages/raystack/components/kbd/kbd.tsx +++ b/packages/raystack/components/kbd/kbd.tsx @@ -1,7 +1,7 @@ 'use client'; -import { cva, cx, type VariantProps } from 'class-variance-authority'; -import { type ComponentProps, createContext, useContext } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import type { ComponentProps } from 'react'; import styles from './kbd.module.css'; const kbd = cva(styles['kbd'], { @@ -10,42 +10,35 @@ const kbd = cva(styles['kbd'], { solid: styles['kbd-solid'], ghost: styles['kbd-ghost'] } - }, - defaultVariants: { - variant: 'solid' } }); -type KbdVariant = NonNullable['variant']>; - -const KbdGroupContext = createContext(undefined); +const kbdGroup = cva(styles['kbd-group'], { + variants: { + variant: { + solid: styles['kbd-group-solid'], + ghost: styles['kbd-group-ghost'] + } + } +}); export type KbdProps = ComponentProps<'kbd'> & VariantProps; -const KbdRoot = ({ className, variant, ...props }: KbdProps) => { - const groupVariant = useContext(KbdGroupContext); - - return ( - - ); -}; +const KbdRoot = ({ className, variant, ...props }: KbdProps) => ( + +); KbdRoot.displayName = 'Kbd'; -export type KbdGroupProps = ComponentProps<'kbd'> & VariantProps; +export type KbdGroupProps = ComponentProps<'kbd'> & + VariantProps; const KbdGroup = ({ className, variant, ...props }: KbdGroupProps) => ( - - - + ); KbdGroup.displayName = 'Kbd.Group'; From 4369888471b40d8308df6af662258d2074a2f868 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 18 Aug 2026 14:04:23 +0530 Subject: [PATCH 5/7] revert: leave Command untouched by the Kbd PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts every Command change on this branch back to the merge-base, so Command.Shortcut stays exactly as it ships today: its own span + markup, no relation to Kbd. The review thread asking to remove Command.Shortcut reads two ways, and the difference matters: Command.Shortcut is public API in the published package, so deleting it is a breaking change with no @deprecated shim (cf. DataTable). Retiring it — if that is what was meant — belongs in its own change, not in the PR that adds Kbd. This branch is now purely additive: Kbd, its styles, tests, and docs. --- .../content/docs/components/command/demo.ts | 37 +++---------------- .../content/docs/components/command/index.mdx | 10 ++++- .../content/docs/components/command/props.ts | 7 +++- .../command/__tests__/command.test.tsx | 28 -------------- .../command/__tests__/data-slots.test.tsx | 10 ++--- .../components/command/command-item.tsx | 2 +- .../components/command/command-misc.tsx | 35 ++++++++++++++++++ .../components/command/command.module.css | 19 ++++++++++ .../raystack/components/command/command.tsx | 8 +++- 9 files changed, 85 insertions(+), 71 deletions(-) diff --git a/apps/www/src/content/docs/components/command/demo.ts b/apps/www/src/content/docs/components/command/demo.ts index fc5ba8ff1..2c60ec0f0 100644 --- a/apps/www/src/content/docs/components/command/demo.ts +++ b/apps/www/src/content/docs/components/command/demo.ts @@ -27,26 +27,14 @@ export const preview = { Actions } - trailingIcon={ - - - - A - - } + trailingIcon={⌘ ⇧ A} onClick={() => setOpen(false)} > Create AOI... } - trailingIcon={ - - - - W - - } + trailingIcon={⌘ ⇧ W} onClick={() => setOpen(false)} disabled > @@ -149,34 +137,19 @@ export const shortcutDemo = { Suggestions - - P - - } + trailingIcon={⌘ P} onClick={() => setOpen(false)} > Profile - - B - - } + trailingIcon={⌘ B} onClick={() => setOpen(false)} > Billing - - S - - } + trailingIcon={⌘ S} onClick={() => setOpen(false)} > Settings diff --git a/apps/www/src/content/docs/components/command/index.mdx b/apps/www/src/content/docs/components/command/index.mdx index 22a5e21ef..f3b03b5f8 100644 --- a/apps/www/src/content/docs/components/command/index.mdx +++ b/apps/www/src/content/docs/components/command/index.mdx @@ -73,7 +73,7 @@ Rendered when no items are visible (all filtered out, or the list is empty). Aut ### Item -A selectable entry. When `value` is omitted and `children` is a string, the string is used as the value. `onClick` fires on pointer click **and** on keyboard `Enter` when the item is highlighted. Pass a [`Kbd`](/docs/components/kbd) as `trailingIcon` to show a keyboard hint — use the `ghost` variant so the keys sit on the item's own background. +A selectable entry. When `value` is omitted and `children` is a string, the string is used as the value. `onClick` fires on pointer click **and** on keyboard `Enter` when the item is highlighted. @@ -95,6 +95,12 @@ Visual divider between groups. The separator is hidden automatically while the u +### Shortcut + +A `` element for keyboard hints. Typically passed as `trailingIcon` on `Command.Item`. + + + ### Dialog `Command.Dialog` is an alias for the Base UI Dialog root. Pair it with `Command.DialogTrigger` and `Command.DialogContent` to render the command menu in a centered modal. @@ -125,6 +131,8 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test | `command-item-label` | The item's label text | | `command-item-trailing-icon` | Trailing icon inside an item (when provided) | | `command-separator` | `Command.Separator` | +| `command-shortcut` | `Command.Shortcut` wrapper | +| `command-shortcut-key` | Each `` key inside a shortcut | | `command-dialog-trigger` | `Command.DialogTrigger` | | `command-dialog-viewport` | Viewport wrapper for the dialog popup | | `command-dialog-content` | `Command.DialogContent` popup | diff --git a/apps/www/src/content/docs/components/command/props.ts b/apps/www/src/content/docs/components/command/props.ts index 1b8fc9751..5b1b54945 100644 --- a/apps/www/src/content/docs/components/command/props.ts +++ b/apps/www/src/content/docs/components/command/props.ts @@ -103,7 +103,7 @@ export interface CommandItemProps { /** Icon rendered before the item label. */ leadingIcon?: React.ReactNode; - /** Node rendered after the item label (e.g. a `Kbd` shortcut hint or an icon). */ + /** Node rendered after the item label (e.g. `Command.Shortcut` or an icon). */ trailingIcon?: React.ReactNode; /** @@ -131,6 +131,11 @@ export interface CommandSeparatorProps { className?: string; } +export interface CommandShortcutProps { + /** Additional CSS class names. */ + className?: string; +} + export interface CommandDialogProps { /** Controlled open state. */ open?: boolean; diff --git a/packages/raystack/components/command/__tests__/command.test.tsx b/packages/raystack/components/command/__tests__/command.test.tsx index 8075d7c0e..2cdefaf0d 100644 --- a/packages/raystack/components/command/__tests__/command.test.tsx +++ b/packages/raystack/components/command/__tests__/command.test.tsx @@ -2,8 +2,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { describe, expect, it, vi } from 'vitest'; -import { Kbd } from '../../kbd'; -import kbdStyles from '../../kbd/kbd.module.css'; import { Command } from '../command'; import styles from '../command.module.css'; @@ -317,30 +315,4 @@ describe('Command', () => { }); }); }); - - describe('Shortcut hints', () => { - it('renders a Kbd.Group passed as trailingIcon', () => { - const { container } = render( - - - - - K - - } - > - Search - - - - ); - expect(screen.getByText('⌘')).toBeInTheDocument(); - expect(screen.getByText('K')).toBeInTheDocument(); - expect(container.querySelectorAll(`.${kbdStyles['kbd']}`)).toHaveLength( - 2 - ); - }); - }); }); diff --git a/packages/raystack/components/command/__tests__/data-slots.test.tsx b/packages/raystack/components/command/__tests__/data-slots.test.tsx index 5b2b1c4cf..8bb13ddb2 100644 --- a/packages/raystack/components/command/__tests__/data-slots.test.tsx +++ b/packages/raystack/components/command/__tests__/data-slots.test.tsx @@ -1,7 +1,6 @@ import { render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { expectSlots, getSlot } from '~/test-utils/data-slots'; -import { Kbd } from '../../kbd'; import { Command } from '../command'; // Mock scrollIntoView for test environment @@ -19,12 +18,7 @@ const BasicCommand = () => ( Suggestions } - trailingIcon={ - - - K - - } + trailingIcon={⌘ K} > Calendar @@ -50,6 +44,8 @@ describe('Command data-slot contract', () => { 'command-item-leading-icon', 'command-item-label', 'command-item-trailing-icon', + 'command-shortcut', + 'command-shortcut-key', 'command-separator' ]); }); diff --git a/packages/raystack/components/command/command-item.tsx b/packages/raystack/components/command/command-item.tsx index 3990d8edb..ffd5609e5 100644 --- a/packages/raystack/components/command/command-item.tsx +++ b/packages/raystack/components/command/command-item.tsx @@ -10,7 +10,7 @@ import { useCommandContext } from './command-root'; export interface CommandItemProps extends AutocompletePrimitive.Item.Props { /** Icon rendered at the start of the item. */ leadingIcon?: ReactNode; - /** Icon rendered at the end of the item (e.g. a `Kbd` shortcut hint). */ + /** Icon rendered at the end of the item (e.g. `Command.Shortcut`). */ trailingIcon?: ReactNode; } diff --git a/packages/raystack/components/command/command-misc.tsx b/packages/raystack/components/command/command-misc.tsx index a4e9f3345..a1563995a 100644 --- a/packages/raystack/components/command/command-misc.tsx +++ b/packages/raystack/components/command/command-misc.tsx @@ -2,6 +2,7 @@ import { Autocomplete as AutocompletePrimitive } from '@base-ui/react/autocomplete'; import { cx } from 'class-variance-authority'; +import { type ComponentProps } from 'react'; import styles from './command.module.css'; import { useCommandContext } from './command-root'; @@ -61,3 +62,37 @@ export const CommandSeparator = ({ ); }; CommandSeparator.displayName = 'Command.Separator'; + +export type CommandShortcutProps = ComponentProps<'span'>; + +export const CommandShortcut = ({ + className, + children, + ...props +}: CommandShortcutProps) => { + const keys = + typeof children === 'string' + ? children.trim().split(/\s+/).filter(Boolean) + : Array.isArray(children) + ? children + : [children]; + + return ( + + {keys.map((key, index) => ( + + {key} + + ))} + + ); +}; +CommandShortcut.displayName = 'Command.Shortcut'; diff --git a/packages/raystack/components/command/command.module.css b/packages/raystack/components/command/command.module.css index 080dbbc3d..0cbdd837e 100644 --- a/packages/raystack/components/command/command.module.css +++ b/packages/raystack/components/command/command.module.css @@ -102,6 +102,25 @@ height: var(--rs-space-5); } +.shortcut { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: var(--rs-space-1); + white-space: nowrap; + font-family: var(--rs-font-body); + font-weight: var(--rs-font-weight-regular); + font-size: var(--rs-font-size-micro); + line-height: var(--rs-line-height-micro); + letter-spacing: var(--rs-letter-spacing-micro); + color: var(--rs-color-foreground-base-tertiary); +} + +.shortcutKey { + font: inherit; + color: inherit; +} + .empty { padding: var(--rs-space-7) var(--rs-space-3); text-align: center; diff --git a/packages/raystack/components/command/command.tsx b/packages/raystack/components/command/command.tsx index a3e197db4..ac01d9e46 100644 --- a/packages/raystack/components/command/command.tsx +++ b/packages/raystack/components/command/command.tsx @@ -7,7 +7,12 @@ import { import { CommandEmpty } from './command-empty'; import { CommandInput } from './command-input'; import { CommandItem } from './command-item'; -import { CommandGroup, CommandLabel, CommandSeparator } from './command-misc'; +import { + CommandGroup, + CommandLabel, + CommandSeparator, + CommandShortcut +} from './command-misc'; import { CommandRoot } from './command-root'; export const Command = Object.assign(CommandRoot, { @@ -18,6 +23,7 @@ export const Command = Object.assign(CommandRoot, { Group: CommandGroup, Label: CommandLabel, Separator: CommandSeparator, + Shortcut: CommandShortcut, Dialog: CommandDialog, DialogTrigger: CommandDialogTrigger, DialogContent: CommandDialogContent From 10632961f8644ee276089f950f818d32f1ece5ff Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 18 Aug 2026 14:19:47 +0530 Subject: [PATCH 6/7] feat(command)!: remove Command.Shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed with @rohanchkrabrty on the review thread: remove the component rather than decoupling it from Kbd. Command.Shortcut existed to split a whitespace-separated string into one per token. It is only ever used as an item's trailingIcon, where a Kbd or Kbd.Group says the same thing without the loop and the isValidElement branch, so items now take those directly. Docs, demos, slot tables, and tests follow. BREAKING CHANGE: Command.Shortcut is removed. Replace `trailingIcon={⌘ K}` with `trailingIcon={K}`. The `command-shortcut` and `command-shortcut-key` data-slots are gone; style the keys via `kbd` / `kbd-group` instead. --- .../content/docs/components/command/demo.ts | 37 ++++++++++++++++--- .../content/docs/components/command/index.mdx | 10 +---- .../content/docs/components/command/props.ts | 7 +--- .../command/__tests__/command.test.tsx | 28 ++++++++++++++ .../command/__tests__/data-slots.test.tsx | 10 +++-- .../components/command/command-item.tsx | 2 +- .../components/command/command-misc.tsx | 35 ------------------ .../components/command/command.module.css | 19 ---------- .../raystack/components/command/command.tsx | 8 +--- 9 files changed, 71 insertions(+), 85 deletions(-) diff --git a/apps/www/src/content/docs/components/command/demo.ts b/apps/www/src/content/docs/components/command/demo.ts index 2c60ec0f0..fc5ba8ff1 100644 --- a/apps/www/src/content/docs/components/command/demo.ts +++ b/apps/www/src/content/docs/components/command/demo.ts @@ -27,14 +27,26 @@ export const preview = { Actions } - trailingIcon={⌘ ⇧ A} + trailingIcon={ + + + + A + + } onClick={() => setOpen(false)} > Create AOI... } - trailingIcon={⌘ ⇧ W} + trailingIcon={ + + + + W + + } onClick={() => setOpen(false)} disabled > @@ -137,19 +149,34 @@ export const shortcutDemo = { Suggestions ⌘ P} + trailingIcon={ + + + P + + } onClick={() => setOpen(false)} > Profile ⌘ B} + trailingIcon={ + + + B + + } onClick={() => setOpen(false)} > Billing ⌘ S} + trailingIcon={ + + + S + + } onClick={() => setOpen(false)} > Settings diff --git a/apps/www/src/content/docs/components/command/index.mdx b/apps/www/src/content/docs/components/command/index.mdx index f3b03b5f8..22a5e21ef 100644 --- a/apps/www/src/content/docs/components/command/index.mdx +++ b/apps/www/src/content/docs/components/command/index.mdx @@ -73,7 +73,7 @@ Rendered when no items are visible (all filtered out, or the list is empty). Aut ### Item -A selectable entry. When `value` is omitted and `children` is a string, the string is used as the value. `onClick` fires on pointer click **and** on keyboard `Enter` when the item is highlighted. +A selectable entry. When `value` is omitted and `children` is a string, the string is used as the value. `onClick` fires on pointer click **and** on keyboard `Enter` when the item is highlighted. Pass a [`Kbd`](/docs/components/kbd) as `trailingIcon` to show a keyboard hint — use the `ghost` variant so the keys sit on the item's own background. @@ -95,12 +95,6 @@ Visual divider between groups. The separator is hidden automatically while the u -### Shortcut - -A `` element for keyboard hints. Typically passed as `trailingIcon` on `Command.Item`. - - - ### Dialog `Command.Dialog` is an alias for the Base UI Dialog root. Pair it with `Command.DialogTrigger` and `Command.DialogContent` to render the command menu in a centered modal. @@ -131,8 +125,6 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test | `command-item-label` | The item's label text | | `command-item-trailing-icon` | Trailing icon inside an item (when provided) | | `command-separator` | `Command.Separator` | -| `command-shortcut` | `Command.Shortcut` wrapper | -| `command-shortcut-key` | Each `` key inside a shortcut | | `command-dialog-trigger` | `Command.DialogTrigger` | | `command-dialog-viewport` | Viewport wrapper for the dialog popup | | `command-dialog-content` | `Command.DialogContent` popup | diff --git a/apps/www/src/content/docs/components/command/props.ts b/apps/www/src/content/docs/components/command/props.ts index 5b1b54945..1b8fc9751 100644 --- a/apps/www/src/content/docs/components/command/props.ts +++ b/apps/www/src/content/docs/components/command/props.ts @@ -103,7 +103,7 @@ export interface CommandItemProps { /** Icon rendered before the item label. */ leadingIcon?: React.ReactNode; - /** Node rendered after the item label (e.g. `Command.Shortcut` or an icon). */ + /** Node rendered after the item label (e.g. a `Kbd` shortcut hint or an icon). */ trailingIcon?: React.ReactNode; /** @@ -131,11 +131,6 @@ export interface CommandSeparatorProps { className?: string; } -export interface CommandShortcutProps { - /** Additional CSS class names. */ - className?: string; -} - export interface CommandDialogProps { /** Controlled open state. */ open?: boolean; diff --git a/packages/raystack/components/command/__tests__/command.test.tsx b/packages/raystack/components/command/__tests__/command.test.tsx index 2cdefaf0d..8075d7c0e 100644 --- a/packages/raystack/components/command/__tests__/command.test.tsx +++ b/packages/raystack/components/command/__tests__/command.test.tsx @@ -2,6 +2,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { describe, expect, it, vi } from 'vitest'; +import { Kbd } from '../../kbd'; +import kbdStyles from '../../kbd/kbd.module.css'; import { Command } from '../command'; import styles from '../command.module.css'; @@ -315,4 +317,30 @@ describe('Command', () => { }); }); }); + + describe('Shortcut hints', () => { + it('renders a Kbd.Group passed as trailingIcon', () => { + const { container } = render( + + + + + K + + } + > + Search + + + + ); + expect(screen.getByText('⌘')).toBeInTheDocument(); + expect(screen.getByText('K')).toBeInTheDocument(); + expect(container.querySelectorAll(`.${kbdStyles['kbd']}`)).toHaveLength( + 2 + ); + }); + }); }); diff --git a/packages/raystack/components/command/__tests__/data-slots.test.tsx b/packages/raystack/components/command/__tests__/data-slots.test.tsx index 8bb13ddb2..5b2b1c4cf 100644 --- a/packages/raystack/components/command/__tests__/data-slots.test.tsx +++ b/packages/raystack/components/command/__tests__/data-slots.test.tsx @@ -1,6 +1,7 @@ import { render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { expectSlots, getSlot } from '~/test-utils/data-slots'; +import { Kbd } from '../../kbd'; import { Command } from '../command'; // Mock scrollIntoView for test environment @@ -18,7 +19,12 @@ const BasicCommand = () => ( Suggestions } - trailingIcon={⌘ K} + trailingIcon={ + + + K + + } > Calendar @@ -44,8 +50,6 @@ describe('Command data-slot contract', () => { 'command-item-leading-icon', 'command-item-label', 'command-item-trailing-icon', - 'command-shortcut', - 'command-shortcut-key', 'command-separator' ]); }); diff --git a/packages/raystack/components/command/command-item.tsx b/packages/raystack/components/command/command-item.tsx index ffd5609e5..3990d8edb 100644 --- a/packages/raystack/components/command/command-item.tsx +++ b/packages/raystack/components/command/command-item.tsx @@ -10,7 +10,7 @@ import { useCommandContext } from './command-root'; export interface CommandItemProps extends AutocompletePrimitive.Item.Props { /** Icon rendered at the start of the item. */ leadingIcon?: ReactNode; - /** Icon rendered at the end of the item (e.g. `Command.Shortcut`). */ + /** Icon rendered at the end of the item (e.g. a `Kbd` shortcut hint). */ trailingIcon?: ReactNode; } diff --git a/packages/raystack/components/command/command-misc.tsx b/packages/raystack/components/command/command-misc.tsx index a1563995a..a4e9f3345 100644 --- a/packages/raystack/components/command/command-misc.tsx +++ b/packages/raystack/components/command/command-misc.tsx @@ -2,7 +2,6 @@ import { Autocomplete as AutocompletePrimitive } from '@base-ui/react/autocomplete'; import { cx } from 'class-variance-authority'; -import { type ComponentProps } from 'react'; import styles from './command.module.css'; import { useCommandContext } from './command-root'; @@ -62,37 +61,3 @@ export const CommandSeparator = ({ ); }; CommandSeparator.displayName = 'Command.Separator'; - -export type CommandShortcutProps = ComponentProps<'span'>; - -export const CommandShortcut = ({ - className, - children, - ...props -}: CommandShortcutProps) => { - const keys = - typeof children === 'string' - ? children.trim().split(/\s+/).filter(Boolean) - : Array.isArray(children) - ? children - : [children]; - - return ( - - {keys.map((key, index) => ( - - {key} - - ))} - - ); -}; -CommandShortcut.displayName = 'Command.Shortcut'; diff --git a/packages/raystack/components/command/command.module.css b/packages/raystack/components/command/command.module.css index 0cbdd837e..080dbbc3d 100644 --- a/packages/raystack/components/command/command.module.css +++ b/packages/raystack/components/command/command.module.css @@ -102,25 +102,6 @@ height: var(--rs-space-5); } -.shortcut { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: var(--rs-space-1); - white-space: nowrap; - font-family: var(--rs-font-body); - font-weight: var(--rs-font-weight-regular); - font-size: var(--rs-font-size-micro); - line-height: var(--rs-line-height-micro); - letter-spacing: var(--rs-letter-spacing-micro); - color: var(--rs-color-foreground-base-tertiary); -} - -.shortcutKey { - font: inherit; - color: inherit; -} - .empty { padding: var(--rs-space-7) var(--rs-space-3); text-align: center; diff --git a/packages/raystack/components/command/command.tsx b/packages/raystack/components/command/command.tsx index ac01d9e46..a3e197db4 100644 --- a/packages/raystack/components/command/command.tsx +++ b/packages/raystack/components/command/command.tsx @@ -7,12 +7,7 @@ import { import { CommandEmpty } from './command-empty'; import { CommandInput } from './command-input'; import { CommandItem } from './command-item'; -import { - CommandGroup, - CommandLabel, - CommandSeparator, - CommandShortcut -} from './command-misc'; +import { CommandGroup, CommandLabel, CommandSeparator } from './command-misc'; import { CommandRoot } from './command-root'; export const Command = Object.assign(CommandRoot, { @@ -23,7 +18,6 @@ export const Command = Object.assign(CommandRoot, { Group: CommandGroup, Label: CommandLabel, Separator: CommandSeparator, - Shortcut: CommandShortcut, Dialog: CommandDialog, DialogTrigger: CommandDialogTrigger, DialogContent: CommandDialogContent From 17293409b7bcca41c944f24c8bc38370f3848c5f Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 18 Aug 2026 15:08:09 +0530 Subject: [PATCH 7/7] docs: stop predicting screen-reader output in Kbd accessibility notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notes claimed "is not exposed as a separate accessible object" and that bare symbols "are read by their Unicode names, if at all" — both assert specific assistive-tech behaviour we have not verified, the same problem as the "screen readers announce as keyboard input" line that was already removed in review. State only what holds by construction: carries no ARIA role or accessible name, a bare glyph carries no key name, and how a given screen reader reads it varies — so name the key when the symbol is the only cue. Also point the ghost-variant examples at a command item rather than a menu row, since Menu has no shortcut slot. --- apps/www/src/content/docs/components/kbd/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/www/src/content/docs/components/kbd/index.mdx b/apps/www/src/content/docs/components/kbd/index.mdx index a07f1a74f..1ddb88659 100644 --- a/apps/www/src/content/docs/components/kbd/index.mdx +++ b/apps/www/src/content/docs/components/kbd/index.mdx @@ -68,7 +68,7 @@ Use `Kbd` on its own for a one-key hint. Keys share a minimum width so a narrow ### 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. +`solid` is the default and suits standalone hints. Use `ghost` on surfaces that already have their own background, such as a command item, a tooltip, or an input. @@ -104,7 +104,7 @@ A common use is surfacing a shortcut alongside the action it triggers. ## Accessibility -- `Kbd` is presentational and renders the semantic `` 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` is presentational: it styles a key. The `` element carries no ARIA role and no accessible name of its own, so it adds no semantics to the text around it. +- A symbol on its own — `⌘`, `⇧`, `↵` — carries no name for the key it stands for, and how any given screen reader reads the bare glyph varies. Give the key a name whenever the symbol is the only cue: ``. - 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. +- Keys ignore pointer events and text selection, so clicking or dragging across a command item does not highlight the key labels.