UNPKG

@loke/design-system

Version:

A design system with individually importable components

379 lines (292 loc) 11.8 kB
--- name: interactive-components description: > Use interactive design system components and the asChild/Slot pattern. Button (variants: default/destructive/ghost/link/outline/secondary, sizes: default/sm/lg/icon/icon-sm/icon-xs/icon-lg, width, justify, asChild), Input (auto-icon detection by type: search/email/password/time/date, onClear callback, custom icon prop), Textarea, Accordion/AccordionItem/ AccordionTrigger/AccordionContent, Tabs/TabsList/TabsTrigger/TabsContent, Collapsible/CollapsibleTrigger/CollapsibleContent, Label, Slot/Slottable/ createSlot for asChild composition. Activate when adding buttons, inputs, accordions, tabs, or using the asChild pattern. type: core library: '@loke/design-system' library_version: '2.0.0-rc.6' requires: - getting-started sources: - 'LOKE/merchant-frontends:packages/design-system/src/components/button' - 'LOKE/merchant-frontends:packages/design-system/src/components/input' - 'LOKE/merchant-frontends:packages/design-system/src/components/textarea' - 'LOKE/merchant-frontends:packages/design-system/src/components/accordion' - 'LOKE/merchant-frontends:packages/design-system/src/components/tabs' - 'LOKE/merchant-frontends:packages/design-system/src/components/collapsible' - 'LOKE/merchant-frontends:packages/design-system/src/components/label' - 'LOKE/merchant-frontends:packages/design-system/src/components/slot' --- # Interactive Components This skill builds on **getting-started**. Read it first for setup and imports. ## Setup ```tsx import { Button } from "@loke/design-system/button"; import { Input } from "@loke/design-system/input"; import { Slot } from "@loke/design-system/slot"; // Button with variant <Button variant="destructive" size="sm">Delete</Button> // Input with auto-icon by type <Input type="search" placeholder="Search..." onClear={() => setValue("")} /> // asChild pattern — render as a link instead of a button <Button asChild> <a href="/docs">Documentation</a> </Button> ``` ## Core Patterns ### Button variants and sizes Button accepts `variant`, `size`, `width`, `justify`, and `asChild` props. **Variants** (6 total): | Variant | Usage | |---|---| | `default` | Primary actions (submit, save) | | `destructive` | Delete, remove, dangerous actions | | `ghost` | Subtle actions, toolbar buttons | | `link` | Text-only link styling with underline on hover | | `outline` | Secondary actions with visible border | | `secondary` | Less prominent actions | **Sizes** (7 total): | Size | Output | |---|---| | `default` | `h-10 px-4 py-2` | | `sm` | `h-9 rounded-md px-3` | | `lg` | `h-11 rounded-md px-8` | | `icon` | `size-8` (square, default icon button) | | `icon-sm` | `size-7` | | `icon-xs` | `size-6` (smallest icon button, svg shrinks to size-3) | | `icon-lg` | `size-9` | **Width and justify:** ```tsx <Button width="full" justify="between"> Select option <ChevronDownIcon /> </Button> ``` **asChild for links** -- prevents nested interactive elements (button > a): ```tsx import { Button } from "@loke/design-system/button"; <Button asChild variant="outline"> <a href="/docs">Go to docs</a> </Button> ``` **Icon buttons:** ```tsx import { Button } from "@loke/design-system/button"; import { Trash2 } from "@loke/icons"; <Button variant="ghost" size="icon-sm"> <Trash2 /> </Button> ``` ### Input with auto-icon and clear `Input` auto-assigns leading icons based on `type`. Pass `icon` to override. Pass `onClear` for a clear button. **Auto-icon mapping:** | `type` | Icon | |---|---| | `search` | `Search` | | `email` | `Mail` | | `password` | `Lock` | | `time` | `Clock` | | `date` / `datetime-local` | `Calendar` | ```tsx import { Input } from "@loke/design-system/input"; // Search with auto icon + clear button <Input type="search" placeholder="Search products..." value={query} onChange={(e) => setQuery(e.target.value)} onClear={() => setQuery("")} /> // Email with auto icon <Input type="email" placeholder="you@example.com" /> // Custom icon override import { DollarSign } from "@loke/icons"; <Input icon={DollarSign} type="number" placeholder="0.00" /> ``` Props: `InputProps = InputHTMLAttributes<HTMLInputElement> & { icon?: LokeIcon; onClear?: () => void }` ### Textarea Standard multi-line input. No special props beyond `TextareaHTMLAttributes<HTMLTextAreaElement>`. ```tsx import { Textarea } from "@loke/design-system/textarea"; <Textarea placeholder="Enter a description..." rows={4} /> ``` ### Accordion Collapsible content panels. Supports `type="single"` (one open at a time) or `type="multiple"` (independent). ```tsx import { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from "@loke/design-system/accordion"; <Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger>What is your refund policy?</AccordionTrigger> <AccordionContent> We offer a 30-day money-back guarantee on all plans. </AccordionContent> </AccordionItem> <AccordionItem value="item-2"> <AccordionTrigger>How do I cancel?</AccordionTrigger> <AccordionContent> Go to Settings and select Cancel Subscription. </AccordionContent> </AccordionItem> </Accordion> ``` - Each `AccordionItem` requires a unique `value` string. - `AccordionTrigger` renders chevron icons automatically (down when closed, up when open). - `collapsible` prop on `Accordion` allows all items to be closed when `type="single"`. ### Tabs Organize content into selectable tab panels. Supports `orientation="horizontal"` (default) and `orientation="vertical"`. ```tsx import { Tabs, TabsList, TabsTrigger, TabsContent, } from "@loke/design-system/tabs"; <Tabs defaultValue="overview"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="analytics">Analytics</TabsTrigger> <TabsTrigger value="settings">Settings</TabsTrigger> </TabsList> <TabsContent value="overview">Overview content here.</TabsContent> <TabsContent value="analytics">Analytics content here.</TabsContent> <TabsContent value="settings">Settings content here.</TabsContent> </Tabs> ``` - `TabsTrigger` `value` must match the corresponding `TabsContent` `value`. - Active tab has an animated underline indicator. - Keyboard navigation (arrow keys) works out of the box. ### Collapsible Simple show/hide toggle for a single section. Lighter than Accordion when you only need one collapsible region. ```tsx import { Collapsible, CollapsibleTrigger, CollapsibleContent, } from "@loke/design-system/collapsible"; import { Button } from "@loke/design-system/button"; <Collapsible> <CollapsibleTrigger asChild> <Button variant="ghost" size="sm">Toggle details</Button> </CollapsibleTrigger> <CollapsibleContent> <p>Additional details shown when expanded.</p> </CollapsibleContent> </Collapsible> ``` ### The asChild / Slot pattern When `asChild={true}`, a component renders its child element instead of its default DOM element, merging all props (className, event handlers, refs, aria attributes) onto the child. **How it works internally:** ```tsx import { createSlot } from "@loke/design-system/slot"; const ButtonSlot = createSlot("Button"); function Button({ asChild, children, className, ...props }) { const Comp = asChild ? ButtonSlot : "button"; return <Comp className={className} {...props}>{children}</Comp>; } ``` **Key exports from `@loke/design-system/slot`:** | Export | Purpose | |---|---| | `Slot` | Merges props into a single child element | | `Slottable` | Marks content as replaceable within a slotted layout | | `createSlot(ownerName)` | Creates a namespaced Slot (e.g., `Button.Slot`) | | `createSlottable(ownerName)` | Creates a namespaced Slottable marker | **Components that support `asChild`:** Button, CollapsibleTrigger, and sidebar-related compositions. **Merge rules:** - Event handlers compose: child handler runs first, then slot handler. - `className` values concatenate. - `style` objects merge (child overrides slot). - `Slot` expects exactly one valid child element. ```tsx import { Slot } from "@loke/design-system/slot"; // Generic polymorphic wrapper function Card({ asChild, className, ...props }) { const Comp = asChild ? Slot : "div"; return <Comp className={cn("rounded-lg border p-4", className)} {...props} />; } // Renders as <section> with merged className <Card asChild> <section className="bg-muted">Custom card</section> </Card> ``` ## Common Mistakes ### 1. CRITICAL: Missing asChild when wrapping non-button elements ```tsx // WRONG -- produces nested interactive elements (button > a), invalid HTML <Button><a href="/docs">Docs</a></Button> // CORRECT <Button asChild><a href="/docs">Docs</a></Button> ``` Without `asChild`, Button renders a `<button>` wrapping an `<a>`, which is invalid HTML and breaks accessibility. ### 2. CRITICAL: Hallucinating props from other libraries ```tsx // WRONG -- none of these props exist on Button <Button isLoading leftIcon={<Spinner />} colorScheme="blue">Save</Button> // CORRECT -- handle loading state yourself <Button disabled={isPending}> {isPending && <Spinner className="mr-2" />} Save </Button> ``` Agents frequently add `isLoading`, `colorScheme`, `leftIcon`, `rightIcon` from shadcn/Chakra/MUI. The `@loke/design-system` Button only accepts `variant`, `size`, `width`, `justify`, `asChild`, and standard HTML button attributes. ### 3. HIGH: Hallucinating components that don't exist ```tsx // WRONG -- none of these exist in @loke/design-system import { Combobox } from "@loke/design-system/combobox"; import { Drawer } from "@loke/design-system/drawer"; import { NavigationMenu } from "@loke/design-system/navigation-menu"; import { ScrollArea } from "@loke/design-system/scroll-area"; import { HoverCard } from "@loke/design-system/hover-card"; import { FormInput } from "@loke/design-system/form-input"; ``` Always verify a component exists by checking `package.json` exports before importing. Combobox is built by composing Popover + Command (see overlay-composition skill). ### 4. HIGH: Wrong Button variant for destructive actions ```tsx // WRONG -- default variant for a delete action <Button onClick={handleDelete}>Delete account</Button> // CORRECT <Button variant="destructive" onClick={handleDelete}>Delete account</Button> // Confirmation dialogs: destructive action + outline cancel <div className="flex gap-2 justify-end"> <Button variant="outline" onClick={onCancel}>Cancel</Button> <Button variant="destructive" onClick={onConfirm}>Delete</Button> </div> ``` ### 5. HIGH: Building custom search input instead of using Input type="search" ```tsx // WRONG -- manually recreating what Input already provides function SearchInput({ value, onChange, onClear }) { return ( <div className="relative"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4" /> <input className="pl-8 ..." value={value} onChange={onChange} /> {value && <button onClick={onClear}><X /></button>} </div> ); } // CORRECT -- Input handles icon + clear button automatically import { Input } from "@loke/design-system/input"; <Input type="search" value={value} onChange={(e) => setValue(e.target.value)} onClear={() => setValue("")} /> ``` `Input` with `type="search"` auto-renders the search icon and, when `onClear` is provided, adds a clear button. No need to build this from scratch. ## See also - **forms/SKILL.md** -- form controls wire differently per component (Label, FormControl, validation) - **overlay-composition/SKILL.md** -- Combobox = Popover + Command; Dialog, Sheet, Popover patterns - **display-components/SKILL.md** -- presentational components (Badge, Card, Avatar, Separator, etc.)