Skip to content

Commit abaefaa

Browse files
VINAYMADIVALVinay Nagesh Madival
andauthored
feat: Add Self-Contained Kbd Component with Search Bar Demo (#217)
* Add self-contained Spinner component and integrate into components data * Add self-contained Kbd component with integrated search bar demo and components data entry * Rename Kbd component entry to 'Keyboard Shortcut' for consistency with naming conventions --------- Co-authored-by: Vinay Nagesh Madival <you@example.com> Co-authored-by: Vinay Nagesh Madival <[email protected]>
1 parent 000965b commit abaefaa

2 files changed

Lines changed: 178 additions & 1 deletion

File tree

src/components/ui/kbd.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import * as React from "react";
2+
import { cn } from "@/lib/utils";
3+
4+
export type KbdProps = React.ComponentProps<"kbd">;
5+
6+
const KbdInner = React.forwardRef<HTMLElement, KbdProps>(
7+
({ className, children, ...props }, ref) => {
8+
return (
9+
<kbd
10+
ref={ref}
11+
{...props}
12+
className={cn(
13+
"inline-flex items-center rounded-md border border-border bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground",
14+
className
15+
)}
16+
>
17+
{children}
18+
</kbd>
19+
);
20+
}
21+
);
22+
KbdInner.displayName = "Kbd";
23+
const KbdPreview: React.FC = () => {
24+
const inputRef = React.useRef<HTMLInputElement | null>(null);
25+
const [focused, setFocused] = React.useState(false);
26+
27+
// handle global shortcut (Ctrl/Cmd + K)
28+
React.useEffect(() => {
29+
const onKey = (e: KeyboardEvent) => {
30+
// normalize key + modifier check
31+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
32+
// prevent browser default (may be browser-specific in some edge cases)
33+
e.preventDefault();
34+
inputRef.current?.focus();
35+
setFocused(true);
36+
}
37+
};
38+
window.addEventListener("keydown", onKey);
39+
return () => window.removeEventListener("keydown", onKey);
40+
}, []);
41+
42+
return (
43+
<div className="w-full max-w-xs">
44+
<div className="relative">
45+
<input
46+
ref={inputRef}
47+
onFocus={() => setFocused(true)}
48+
onBlur={() => setFocused(false)}
49+
aria-label="Search"
50+
placeholder="Search..."
51+
className="w-full rounded-md border border-border bg-input px-3 py-2 pl-8 pr-28 text-sm placeholder:text-muted-foreground focus:outline-none"
52+
/>
53+
54+
{/* left search icon (inside the bar) */}
55+
<div className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2">
56+
<svg
57+
xmlns="http://www.w3.org/2000/svg"
58+
viewBox="0 0 24 24"
59+
className="size-4 opacity-80"
60+
aria-hidden="true"
61+
width="16"
62+
height="16"
63+
>
64+
<path
65+
d="M21 21l-4.35-4.35"
66+
stroke="currentColor"
67+
strokeWidth="2"
68+
strokeLinecap="round"
69+
strokeLinejoin="round"
70+
fill="none"
71+
/>
72+
<circle
73+
cx="11"
74+
cy="11"
75+
r="6"
76+
stroke="currentColor"
77+
strokeWidth="2"
78+
fill="none"
79+
/>
80+
</svg>
81+
</div>
82+
{!focused && (
83+
<div
84+
onMouseDown={(e) => {
85+
// ensure click focuses input (use mousedown to avoid losing focus)
86+
e.preventDefault();
87+
inputRef.current?.focus();
88+
setFocused(true);
89+
}}
90+
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2"
91+
aria-hidden
92+
>
93+
<KbdInner></KbdInner>
94+
<KbdInner>K</KbdInner>
95+
</div>
96+
)}
97+
</div>
98+
</div>
99+
);
100+
};
101+
102+
// Expose Preview as a static property so data file only imports { Kbd }
103+
type KbdComponentType = typeof KbdInner & { Preview: React.FC };
104+
105+
const Kbd = KbdInner as KbdComponentType;
106+
Kbd.Preview = KbdPreview;
107+
108+
export { Kbd };

src/data/components.tsx

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ import {
9191

9292
import { Spinner } from "@/components/ui/spinner";
9393

94+
import { Kbd } from "@/components/ui/kbd";
95+
9496
export const componentsData = [
9597
{
9698
id: "button",
@@ -1979,7 +1981,7 @@ export function CollapsibleDemo() {
19791981
</div>
19801982
</div>
19811983
),
1982-
code: `import { Spinner } from "@/components/Spinner"
1984+
code: `import { Spinner } from "@/components/ui/spinner"
19831985
19841986
export function SpinnerDemo() {
19851987
return (
@@ -1998,4 +2000,71 @@ export function SpinnerDemo() {
19982000
{ name: "label", type: "string", description: "Accessible label for screen readers.", default: "Loading" },
19992001
],
20002002
},
2003+
{
2004+
id: "keyboard-shortcut",
2005+
title: "Keyboard Shortcut",
2006+
description: "A small self-contained keyboard key element for displaying shortcuts.",
2007+
category: "Display",
2008+
preview: <Kbd.Preview />,
2009+
code: `import { Kbd } from "@/components/ui/kbd"
2010+
2011+
export function KbdDemo() {
2012+
return (
2013+
<div className="w-full max-w-xs">
2014+
<div className="relative">
2015+
<input
2016+
aria-label="Search"
2017+
placeholder="Search..."
2018+
className="w-full rounded-md border border-border bg-input px-3 py-2 pl-8 pr-28 text-sm placeholder:text-muted-foreground focus:outline-none"
2019+
/>
2020+
<div className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2">
2021+
<svg
2022+
xmlns="http://www.w3.org/2000/svg"
2023+
viewBox="0 0 24 24"
2024+
className="size-4 opacity-80"
2025+
aria-hidden="true"
2026+
width="16"
2027+
height="16"
2028+
>
2029+
<path
2030+
d="M21 21l-4.35-4.35"
2031+
stroke="currentColor"
2032+
strokeWidth="2"
2033+
strokeLinecap="round"
2034+
strokeLinejoin="round"
2035+
fill="none"
2036+
/>
2037+
<circle
2038+
cx="11"
2039+
cy="11"
2040+
r="6"
2041+
stroke="currentColor"
2042+
strokeWidth="2"
2043+
fill="none"
2044+
/>
2045+
</svg>
2046+
</div>
2047+
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
2048+
<Kbd>Ctrl</Kbd>
2049+
<Kbd>K</Kbd>
2050+
</div>
2051+
</div>
2052+
</div>
2053+
)
2054+
}`,
2055+
propsData: [
2056+
{
2057+
name: "children",
2058+
type: "ReactNode",
2059+
description: "Content inside the Kbd element (e.g., key label).",
2060+
required: true,
2061+
},
2062+
{
2063+
name: "className",
2064+
type: "string",
2065+
description: "Additional classes to customize appearance.",
2066+
default: '""',
2067+
},
2068+
],
2069+
},
20012070
];

0 commit comments

Comments
 (0)