From 4a644247ec71fadd9ba48462fca00373d5a70a87 Mon Sep 17 00:00:00 2001 From: Vinay Nagesh Madival Date: Mon, 13 Oct 2025 09:24:18 +0530 Subject: [PATCH 1/5] Add self-contained Spinner component and integrate into components data --- src/components/ui/spinner.tsx | 35 ++++++++++++++++++++++++++++++++++ src/data/components.tsx | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/components/ui/spinner.tsx diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx new file mode 100644 index 0000000..980baef --- /dev/null +++ b/src/components/ui/spinner.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export type SpinnerProps = React.HTMLAttributes & { + size?: "sm" | "md" | "lg"; + label?: string; +}; + +export const Spinner = React.forwardRef( + ({ className, size = "md", label = "Loading", ...props }, ref) => { + const sizeClass = + size === "sm" + ? "h-4 w-4 border-2" + : size === "lg" + ? "h-8 w-8 border-2" + : "h-5 w-5 border-2"; + + return ( +
+ ); + } +); + +Spinner.displayName = "Spinner"; +export default Spinner; diff --git a/src/data/components.tsx b/src/data/components.tsx index f192498..474a83e 100644 --- a/src/data/components.tsx +++ b/src/data/components.tsx @@ -89,6 +89,8 @@ import { PopoverTrigger, } from "@/components/ui/popover"; +import { Spinner } from "@/components/ui/spinner"; + export const componentsData = [ { id: "button", @@ -1962,4 +1964,38 @@ export function CollapsibleDemo() { }, ], }, + { + id: "spinner", + title: "Spinner", + description: "A small self-contained spinner used to indicate loading states.", + category: "Feedback", + preview: ( +
+ +
+ + + +
+
+ ), + code: `import { Spinner } from "@/components/Spinner" + +export function SpinnerDemo() { + return ( +
+ +
+ + + +
+
+ ) +}`, + propsData: [ + { name: "size", type: '"sm" | "md" | "lg"', description: "Size preset for the spinner.", default: "md" }, + { name: "label", type: "string", description: "Accessible label for screen readers.", default: "Loading" }, + ], + }, ]; From f9b200ac79279221e273d723809ca002b13f8b11 Mon Sep 17 00:00:00 2001 From: Vinay Nagesh Madival <[email protected]> Date: Mon, 13 Oct 2025 23:44:04 +0530 Subject: [PATCH 2/5] Add self-contained Kbd component with integrated search bar demo and components data entry --- src/components/ui/kbd.tsx | 108 ++++++++++++++++++++++++++++++++++++++ src/data/components.tsx | 71 ++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/kbd.tsx diff --git a/src/components/ui/kbd.tsx b/src/components/ui/kbd.tsx new file mode 100644 index 0000000..8c3835a --- /dev/null +++ b/src/components/ui/kbd.tsx @@ -0,0 +1,108 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export type KbdProps = React.ComponentProps<"kbd">; + +const KbdInner = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( + + {children} + + ); + } +); +KbdInner.displayName = "Kbd"; +const KbdPreview: React.FC = () => { + const inputRef = React.useRef(null); + const [focused, setFocused] = React.useState(false); + + // handle global shortcut (Ctrl/Cmd + K) + React.useEffect(() => { + const onKey = (e: KeyboardEvent) => { + // normalize key + modifier check + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + // prevent browser default (may be browser-specific in some edge cases) + e.preventDefault(); + inputRef.current?.focus(); + setFocused(true); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + return ( +
+
+ setFocused(true)} + onBlur={() => setFocused(false)} + aria-label="Search" + placeholder="Search..." + 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" + /> + + {/* left search icon (inside the bar) */} +
+ +
+ {!focused && ( +
{ + // ensure click focuses input (use mousedown to avoid losing focus) + e.preventDefault(); + inputRef.current?.focus(); + setFocused(true); + }} + className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2" + aria-hidden + > + + K +
+ )} +
+
+ ); +}; + +// Expose Preview as a static property so data file only imports { Kbd } +type KbdComponentType = typeof KbdInner & { Preview: React.FC }; + +const Kbd = KbdInner as KbdComponentType; +Kbd.Preview = KbdPreview; + +export { Kbd }; diff --git a/src/data/components.tsx b/src/data/components.tsx index 474a83e..d93bc68 100644 --- a/src/data/components.tsx +++ b/src/data/components.tsx @@ -91,6 +91,8 @@ import { import { Spinner } from "@/components/ui/spinner"; +import { Kbd } from "@/components/ui/kbd"; + export const componentsData = [ { id: "button", @@ -1979,7 +1981,7 @@ export function CollapsibleDemo() {
), - code: `import { Spinner } from "@/components/Spinner" + code: `import { Spinner } from "@/components/ui/spinner" export function SpinnerDemo() { return ( @@ -1998,4 +2000,71 @@ export function SpinnerDemo() { { name: "label", type: "string", description: "Accessible label for screen readers.", default: "Loading" }, ], }, + { + id: "kbd", + title: "Kbd", + description: "A small self-contained keyboard key element for displaying shortcuts.", + category: "Display", + preview: , + code: `import { Kbd } from "@/components/ui/kbd" + +export function KbdDemo() { + return ( +
+
+ +
+ +
+
+ Ctrl + K +
+
+
+ ) +}`, + propsData: [ + { + name: "children", + type: "ReactNode", + description: "Content inside the Kbd element (e.g., key label).", + required: true, + }, + { + name: "className", + type: "string", + description: "Additional classes to customize appearance.", + default: '""', + }, + ], + }, ]; From 13bf50e0b4648476a89867a4eefba498d41ab472 Mon Sep 17 00:00:00 2001 From: Vinay Nagesh Madival <[email protected]> Date: Tue, 14 Oct 2025 07:46:12 +0530 Subject: [PATCH 3/5] Rename Kbd component entry to 'Keyboard Shortcut' for consistency with naming conventions --- src/data/components.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/components.tsx b/src/data/components.tsx index d93bc68..d6cd0fb 100644 --- a/src/data/components.tsx +++ b/src/data/components.tsx @@ -2001,8 +2001,8 @@ export function SpinnerDemo() { ], }, { - id: "kbd", - title: "Kbd", + id: "keyboard-shortcut", + title: "Keyboard Shortcut", description: "A small self-contained keyboard key element for displaying shortcuts.", category: "Display", preview: , From 8f0104c400e447cdb8ec8c09e8cca510eb2813be Mon Sep 17 00:00:00 2001 From: Vinay Nagesh Madival <[email protected]> Date: Tue, 14 Oct 2025 19:54:29 +0530 Subject: [PATCH 4/5] Add self-contained Input OTP component with 3-3 digit layout and integrated demo --- package-lock.json | 11 ++ package.json | 1 + src/components/ui/input-otp.tsx | 250 ++++++++++++++++++++++++++++++++ src/data/components.tsx | 37 +++++ 4 files changed, 299 insertions(+) create mode 100644 src/components/ui/input-otp.tsx diff --git a/package-lock.json b/package-lock.json index 7bb447d..b9ad87f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "input-otp": "^1.4.2", "lucide-react": "^0.544.0", "next": "15.5.4", "next-themes": "^0.4.6", @@ -2919,6 +2920,16 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", diff --git a/package.json b/package.json index db9a68e..d66f586 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "input-otp": "^1.4.2", "lucide-react": "^0.544.0", "next": "15.5.4", "next-themes": "^0.4.6", diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx new file mode 100644 index 0000000..f940f7c --- /dev/null +++ b/src/components/ui/input-otp.tsx @@ -0,0 +1,250 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +/* ---------- Context ---------- */ +type OTPContextValue = { + maxLength: number; + values: string[]; + setAt: (index: number, char: string) => void; + focusIndex: (index: number) => void; + registerRef: (index: number, el: HTMLInputElement | null) => void; + handlePaste: (index: number, text: string) => void; +}; +const OTPContext = React.createContext(null); +function useOTP() { + const ctx = React.useContext(OTPContext); + if (!ctx) throw new Error("InputOTP components must be used inside "); + return ctx; +} + +/* ---------- InputOTP (provider) ---------- */ +export type InputOTPProps = { + maxLength?: number; + children?: React.ReactNode; +} & React.HTMLAttributes; + +const InputOTPInner = React.forwardRef( + ({ maxLength = 6, children, ...props }, ref) => { + const [values, setValues] = React.useState( + () => Array.from({ length: maxLength }).map(() => "") + ); + + // refs for each input slot + const refs = React.useRef>([]); + refs.current = refs.current.slice(0, maxLength); + + const registerRef = React.useCallback((index: number, el: HTMLInputElement | null) => { + refs.current[index] = el; + }, []); + + const focusIndex = React.useCallback((index: number) => { + const safe = Math.max(0, Math.min(index, maxLength - 1)); + refs.current[safe]?.focus(); + refs.current[safe]?.select(); + }, [maxLength]); + + const setAt = React.useCallback( + (index: number, char: string) => { + setValues((prev) => { + const copy = [...prev]; + copy[index] = char; + return copy; + }); + }, + [] + ); + + // handle paste: fill from index forward + const handlePaste = React.useCallback( + (index: number, text: string) => { + const chars = text.split("").slice(0, maxLength - index); + setValues((prev) => { + const copy = [...prev]; + for (let i = 0; i < chars.length; i++) { + copy[index + i] = chars[i]; + } + return copy; + }); + const nextFocus = Math.min(maxLength - 1, index + chars.length); + // focus next (if filled full, focus last) + setTimeout(() => { + refs.current[nextFocus]?.focus(); + refs.current[nextFocus]?.select(); + }, 0); + }, + [maxLength] + ); + + const value: OTPContextValue = React.useMemo( + () => ({ maxLength, values, setAt, focusIndex, registerRef, handlePaste }), + [maxLength, values, setAt, focusIndex, registerRef, handlePaste] + ); + + return ( + + {/* The OTP wrapper, a single horizontal row that does not wrap */} +
+ {children} +
+
+ ); + } +); +InputOTPInner.displayName = "InputOTP"; + +/* ---------- InputOTPGroup ---------- */ +const InputOTPGroup: React.FC> = ({ children, className, ...props }) => { + return ( +
+ {children} +
+ ); +}; + +/* ---------- InputOTPSeparator ---------- */ +const InputOTPSeparator: React.FC> = ({ className, ...props }) => { + return ( + + {/* brighter, centered hyphen */} + + + ); +}; + + +/* ---------- InputOTPSlot ---------- */ +type InputOTPSlotProps = { + index: number; +} & React.InputHTMLAttributes; + +const InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => { + const { maxLength, values, setAt, focusIndex, registerRef, handlePaste } = useOTP(); + + // local ref merging + const innerRef = React.useRef(null); + React.useImperativeHandle(ref, () => innerRef.current as HTMLInputElement); + + React.useEffect(() => { + registerRef(index, innerRef.current); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [index, registerRef]); + + const onChange = (e: React.ChangeEvent) => { + const raw = e.target.value; + if (!raw) { + setAt(index, ""); + return; + } + // we accept only the last character typed (typical OTP behavior) + const char = raw.slice(-1); + setAt(index, char); + // move focus to next + const next = index + 1; + if (next < maxLength) { + focusIndex(next); + } + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Backspace") { + e.preventDefault(); + if (values[index]) { + // clear current + setAt(index, ""); + // keep focus here + setTimeout(() => innerRef.current?.focus(), 0); + } else { + // move to previous and clear it + const prev = index - 1; + if (prev >= 0) { + setAt(prev, ""); + focusIndex(prev); + } + } + } else if (e.key === "ArrowLeft") { + e.preventDefault(); + focusIndex(index - 1); + } else if (e.key === "ArrowRight") { + e.preventDefault(); + focusIndex(index + 1); + } + }; + + const onPaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const text = e.clipboardData.getData("text").trim(); + if (!text) return; + handlePaste(index, text); + }; + + return ( + + ); +}); +InputOTPSlot.displayName = "InputOTPSlot"; + +/* ---------- Preview ---------- */ +const InputOTPPreview: React.FC = () => { + // build a centered demo that shows 3 - 3 with hyphen + return ( +
+
+ + + + + + + + + + + + + + + +
+
+ ); +}; + +/* ---------- Attach Preview and exports ---------- */ +type InputOTPType = typeof InputOTPInner & { Preview: React.FC }; +const InputOTP = InputOTPInner as InputOTPType; +InputOTP.Preview = InputOTPPreview; + +export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot }; +export default InputOTP; diff --git a/src/data/components.tsx b/src/data/components.tsx index d6cd0fb..0a10610 100644 --- a/src/data/components.tsx +++ b/src/data/components.tsx @@ -93,6 +93,8 @@ import { Spinner } from "@/components/ui/spinner"; import { Kbd } from "@/components/ui/kbd"; +import { InputOTP } from "@/components/ui/input-otp"; + export const componentsData = [ { id: "button", @@ -2067,4 +2069,39 @@ export function KbdDemo() { }, ], }, + { + id: "input-otp", + title: "Input OTP", + description: "An OTP input split into two groups (3-3) separated by a hyphen; supports paste, navigation, and backspace behavior.", + category: "Form", + preview: , + code: `import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/components/InputOTP" + +export function InputOTPDemo() { + return ( + + + + + + + + + + + + + + ) +}`, + propsData: [ + { name: "maxLength", type: "number", description: "Total number of OTP digits.", default: "6" }, + { name: "children", type: "ReactNode", description: "Slot groups and separators." }, + ], + }, ]; From db4177d6e215d568a4550ba0ad416ee0c54a290f Mon Sep 17 00:00:00 2001 From: VINAYMADIVAL Date: Tue, 14 Oct 2025 21:08:39 +0530 Subject: [PATCH 5/5] Add self-contained Resizable Panels component with keyboard & pointer support --- package-lock.json | 11 ++ package.json | 1 + src/components/ui/resizable.tsx | 301 ++++++++++++++++++++++++++++++++ src/data/components.tsx | 40 +++++ 4 files changed, 353 insertions(+) create mode 100644 src/components/ui/resizable.tsx diff --git a/package-lock.json b/package-lock.json index b9ad87f..6709fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react": "19.1.0", "react-day-picker": "^9.11.0", "react-dom": "19.1.0", + "react-resizable-panels": "^3.0.6", "react-syntax-highlighter": "^5.8.0", "recharts": "^3.2.1", "shiki": "^3.13.0", @@ -3638,6 +3639,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz", + "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", diff --git a/package.json b/package.json index d66f586..c57f0d2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react": "19.1.0", "react-day-picker": "^9.11.0", "react-dom": "19.1.0", + "react-resizable-panels": "^3.0.6", "react-syntax-highlighter": "^5.8.0", "recharts": "^3.2.1", "shiki": "^3.13.0", diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx new file mode 100644 index 0000000..0dcf3fd --- /dev/null +++ b/src/components/ui/resizable.tsx @@ -0,0 +1,301 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +/* ---------- Types ---------- */ +type Direction = "horizontal" | "vertical"; + +type ResizablePanelGroupProps = { + direction?: Direction; + className?: string; + children: React.ReactNode; +}; + +type ResizablePanelProps = { + defaultSize?: number; // percent for this panel (0-100) + children?: React.ReactNode; +} & React.HTMLAttributes; + +type ResizableHandleProps = { + withHandle?: boolean; +} & React.HTMLAttributes; + +/* ---------- Group ---------- */ +const ResizablePanelGroupInner = React.forwardRef( + ({ direction = "horizontal", children, className, ...props }, ref) => { + // Expect three children: panel, handle, panel + const childs = React.Children.toArray(children) as React.ReactElement[]; + + // read default sizes from first/last panel props if provided + const leftDefault = + (childs[0] && (childs[0].props as any)?.defaultSize) ?? 50; + const rightDefault = + (childs[2] && (childs[2].props as any)?.defaultSize) ?? + Math.max(0, 100 - leftDefault); + + const [primarySize, setPrimarySize] = React.useState( + Math.max(0, Math.min(100, leftDefault)) + ); + + const containerRef = React.useRef(null); + const draggingRef = React.useRef(false); + + // expose resize function for handle + const startDrag = React.useCallback(() => { + draggingRef.current = true; + }, []); + + const stopDrag = React.useCallback(() => { + draggingRef.current = false; + }, []); + + React.useEffect(() => { + const onPointerMove = (e: PointerEvent) => { + if (!draggingRef.current || !containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + if (direction === "horizontal") { + const x = e.clientX - rect.left; + const percent = (x / rect.width) * 100; + setPrimarySize(Math.max(5, Math.min(95, percent))); + } else { + const y = e.clientY - rect.top; + const percent = (y / rect.height) * 100; + setPrimarySize(Math.max(5, Math.min(95, percent))); + } + }; + + const onPointerUp = () => { + draggingRef.current = false; + }; + + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerUp); + return () => { + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("pointerup", onPointerUp); + }; + }, [direction]); + + // keyboard adjustments for the handle (exposed via context) + const adjust = React.useCallback( + (deltaPercent: number) => { + setPrimarySize((prev) => Math.max(5, Math.min(95, prev + deltaPercent))); + }, + [] + ); + + // Provide a simple context so handle/panels can communicate (optional) + const ctx = React.useMemo( + () => ({ + direction, + primarySize, + startDrag, + stopDrag, + adjust, + }), + [direction, primarySize, startDrag, stopDrag, adjust] + ); + + return ( + +
{ + containerRef.current = el; + if (typeof ref === "function") ref(el); + else if (ref) (ref as React.MutableRefObject).current = el; + }} + className={cn( + direction === "horizontal" ? "inline-flex" : "inline-flex flex-col", + "items-stretch", + "overflow-hidden", + className + )} + {...props} + > + {/* render children but inject sizes for first and last panel */} + {React.Children.map(children, (child, idx) => { + if (!React.isValidElement(child)) return child; + // first panel: style flex-basis from primarySize + if (idx === 0) { + const style = + direction === "horizontal" + ? { flexBasis: `${primarySize}%`, minWidth: 0 } + : { flexBasis: `${primarySize}%`, minHeight: 0 }; + return React.cloneElement(child as React.ReactElement, { style }); + } + // third panel: inverse + if (idx === 2) { + const style = + direction === "horizontal" + ? { flexBasis: `${100 - primarySize}%`, minWidth: 0 } + : { flexBasis: `${100 - primarySize}%`, minHeight: 0 }; + return React.cloneElement(child as React.ReactElement, { style }); + } + // handle or other children: render as-is + return child; + })} +
+
+ ); + } +); +ResizablePanelGroupInner.displayName = "ResizablePanelGroup"; + +/* ---------- Context ---------- */ +type ResizableContext = { + direction: Direction; + primarySize: number; + startDrag: () => void; + stopDrag: () => void; + adjust: (deltaPercent: number) => void; +} | null; + +const ResizableContext = React.createContext(null); +const useResizable = () => { + const c = React.useContext(ResizableContext); + if (!c) throw new Error("Resizable components must be used inside "); + return c; +}; + +/* ---------- Panel ---------- */ +const ResizablePanel = React.forwardRef( + ({ defaultSize = 50, children, className, style, ...props }, ref) => { + // panel is mostly a styled container; defaultSize is consumed by the group + return ( +
+ {children} +
+ ); + } +); +ResizablePanel.displayName = "ResizablePanel"; + +/* ---------- Handle ---------- */ +const ResizableHandle = React.forwardRef( + ({ withHandle = false, className, ...props }, ref) => { + const { direction, primarySize, startDrag, stopDrag, adjust } = useResizable(); + + // pointer down initiates dragging + const onPointerDown: React.PointerEventHandler = (e) => { + // only primary button + if (e.button !== 0) return; + (e.target as Element).setPointerCapture?.((e as any).pointerId); + startDrag(); + }; + + const onPointerUp: React.PointerEventHandler = () => { + stopDrag(); + }; + + // keyboard support: arrow keys move handle + const onKeyDown: React.KeyboardEventHandler = (e) => { + const step = 2; // percent per key press + if (direction === "horizontal") { + if (e.key === "ArrowLeft") { + e.preventDefault(); + adjust(-step); + } else if (e.key === "ArrowRight") { + e.preventDefault(); + adjust(step); + } + } else { + if (e.key === "ArrowUp") { + e.preventDefault(); + adjust(-step); + } else if (e.key === "ArrowDown") { + e.preventDefault(); + adjust(step); + } + } + }; + + // aria values: show current primary panel size + const ariaOrientation = direction === "horizontal" ? "horizontal" : "vertical"; + + return ( +
+ {/* optional visible handle button */} + {withHandle ? ( +
+ {/* three dots like a grip */} +
+ + + +
+
+ ) : ( + // a thin bar for visual separation +
+ )} +
+ ); + } +); +ResizableHandle.displayName = "ResizableHandle"; + +/* ---------- Preview built-in (so components.tsx imports only group) ---------- */ +const ResizablePreview: React.FC = () => { + return ( +
+ + +
+ Sidebar +
+
+ + +
+ Content +
+
+
+
+ ); +}; + +/* ---------- Attach preview and exports ---------- */ +type RGroup = typeof ResizablePanelGroupInner & { Preview: React.FC }; +const ResizablePanelGroup = ResizablePanelGroupInner as RGroup; +ResizablePanelGroup.Preview = ResizablePreview; + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; +export default ResizablePanelGroup; diff --git a/src/data/components.tsx b/src/data/components.tsx index 0a10610..58af4c3 100644 --- a/src/data/components.tsx +++ b/src/data/components.tsx @@ -95,6 +95,8 @@ import { Kbd } from "@/components/ui/kbd"; import { InputOTP } from "@/components/ui/input-otp"; +import { ResizablePanelGroup } from "@/components/ui/resizable"; + export const componentsData = [ { id: "button", @@ -2104,4 +2106,42 @@ export function InputOTPDemo() { { name: "children", type: "ReactNode", description: "Slot groups and separators." }, ], }, + { + id: "resizable-panels", + title: "Resizable Panels", + description: "Accessible, resizable panel group with keyboard and pointer support (horizontal/vertical).", + category: "Layout", + preview: , + code: `import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable" + +export function ResizableHandleDemo() { + return ( + + +
+ Sidebar +
+
+ + +
+ Content +
+
+
+ ) +}`, + propsData: [ + { name: "direction", type: '"horizontal" | "vertical"', description: "Resize axis.", default: "horizontal" }, + { name: "className", type: "string", description: "Additional classes for wrapper.", default: '""' }, + { name: "ResizablePanel.defaultSize", type: "number", description: "Default percentage size for a panel.", default: "50" }, + ], + }, ];