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/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..c5a6f6dca --- /dev/null +++ b/apps/www/src/content/docs/components/kbd/demo.ts @@ -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 `${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 + + ` +}; + +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 withInputDemo = { + type: 'code', + code: `⌘K} + />` +}; + +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..1ddb88659 --- /dev/null +++ b/apps/www/src/content/docs/components/kbd/index.mdx @@ -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"; + + + +## 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 + +Groups multiple keyboard keys for key combinations. + + + +### 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 `⌘`. + + + +### Variants + +`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. + + + +### Sequences + +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. + + + +### 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. + + + +### Inline with text + +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. + + + +## Accessibility + +- `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 command item 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..8fa99e84b --- /dev/null +++ b/apps/www/src/content/docs/components/kbd/props.ts @@ -0,0 +1,30 @@ +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'; + + /** 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 inherited by every key in the group. A key's own + * `variant` takes precedence over it. + * @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..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 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..65619b3e0 --- /dev/null +++ b/packages/raystack/components/kbd/__tests__/kbd.test.tsx @@ -0,0 +1,185 @@ +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('Variants', () => { + 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', () => { + const kbd = render(Ctrl).getByText('Ctrl'); + expect(kbd).toHaveClass(styles['kbd-ghost']); + expect(kbd).not.toHaveClass(styles['kbd-solid']); + }); + + it('marks the group so its keys inherit the variant in CSS', () => { + const { container } = render( + + + K + + ); + 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', () => { + render( + + + K + + ); + expect(screen.getByText('⌘')).toHaveClass(styles['kbd-solid']); + expect(screen.getByText('K')).not.toHaveClass(styles['kbd-solid']); + }); + + it('leaves a group with no variant unmarked', () => { + const { container } = render( + + K + + ); + 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', () => { + 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( + + + 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..a170b82e2 --- /dev/null +++ b/packages/raystack/components/kbd/index.tsx @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..da07dfdd2 --- /dev/null +++ b/packages/raystack/components/kbd/kbd.module.css @@ -0,0 +1,42 @@ +.kbd, +.kbd-group { + display: inline-flex; + align-items: center; + width: fit-content; + pointer-events: none; + user-select: none; +} + +.kbd { + justify-content: center; + box-sizing: border-box; + height: var(--rs-space-6); + min-width: var(--rs-space-6); + padding: 0 var(--rs-space-2); + border-radius: var(--rs-radius-1); + 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; + background: var(--rs-color-background-neutral-primary); + color: var(--rs-color-foreground-base-secondary); +} + +:where(.kbd-group-solid) .kbd, +.kbd.kbd-solid { + background: var(--rs-color-background-neutral-primary); + color: var(--rs-color-foreground-base-secondary); +} + +:where(.kbd-group-ghost) .kbd, +.kbd.kbd-ghost { + background: transparent; + color: var(--rs-color-foreground-base-tertiary); +} + +.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 new file mode 100644 index 000000000..9f4109bef --- /dev/null +++ b/packages/raystack/components/kbd/kbd.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { cva, type VariantProps } from 'class-variance-authority'; +import type { ComponentProps } from 'react'; +import styles from './kbd.module.css'; + +const kbd = cva(styles['kbd'], { + variants: { + variant: { + solid: styles['kbd-solid'], + ghost: styles['kbd-ghost'] + } + } +}); + +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) => ( + +); + +KbdRoot.displayName = 'Kbd'; + +export type KbdGroupProps = ComponentProps<'kbd'> & + VariantProps; + +const KbdGroup = ({ className, variant, ...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';