@loke/design-system
Version:
A design system with individually importable components
379 lines (292 loc) • 11.8 kB
Markdown
---
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: '/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 `/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 `/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.)