Interface kit
@wamp/ui is the design system the host itself is built from. An extension page
that uses it looks like part of the application, follows the user’s theme without
any work, and stays correct when the theme changes underneath it. A page that
hand-rolls its own styling looks like an embedded website, and breaks the first
time someone switches to a light theme.
After this page you know what the kit exports, how to import it, which utility classes are backed by theme tokens, and the three rules that keep a page looking native.
How you get it
Section titled “How you get it”Import it. That is the whole setup.
import { Button, Card, EmptyState, Input } from '@wamp/ui';There is nothing to install, no stylesheet to import, and no Tailwind config to
write. Your extension’s page renders inside the host’s own document, so the
tokens and the compiled stylesheet are already there, and the host runs the
Tailwind pass over your source with the kit’s preset applied — which is why
bg-card and rounded-lg resolve to WAMP’s values rather than stock Tailwind’s.
Icons come from lucide-react, which is available at runtime the same way:
import { Plus, Trash2 } from 'lucide-react';What the kit exports
Section titled “What the kit exports”Import only from this list. Anything else makes the build fail with
"X" does not exist in "@wamp/ui".
| Group | Exports |
|---|---|
| Buttons and actions | Button, IconButton, Chip, SegmentedControl |
| Inputs | Input, Textarea, SearchInput, Label, Checkbox, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton |
| Layout | Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Separator, ScrollArea, ScrollBar, Tabs, TabsList, TabsTrigger, TabsContent, Accordion, AccordionItem, AccordionTrigger, AccordionContent, Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption |
| Overlays | Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, DialogClose, DialogPortal, DialogOverlay, ConfirmDialog, Sheet (+ SheetTrigger, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, SheetClose, SheetPortal, SheetOverlay), Popover (+ PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose), DropdownMenu (+ 14 parts), Tooltip, TooltipTrigger, TooltipContent, TooltipProvider |
| Status and feedback | Badge, Spinner, Skeleton, Kbd, EmptyState, ErrorState, LoadingCards, LoadingList, SectionLabel, Toast (+ ToastProvider, ToastViewport, ToastTitle, ToastDescription, ToastClose, ToastAction) |
| App chrome | AppShell with AppShell.TitleBar, AppShell.BackButton, AppShell.Title, AppShell.Content |
| Content | Markdown, TextOutput, WebPreview |
| Chat | Chat, ChatThread, ChatComposer, ModelPicker, ProviderIcon |
| Tool cards | ToolCard, ToolCardShell, ToolCardHeader, ToolCardError, ToolBody, ToolDetail, ToolChain, CompactToolRow, StatusIndicator, ExpandableContent, ExpandedToolOverlay, DiffLinesView, ScreenshotCard, ScreenshotImage |
| Hooks | useAISession, useDataQuery, useToggleState, usePageParams |
| Utilities | cn, renderIcon, formatRelative, messageText, preloadHighlightLanguage, buttonVariants, badgeVariants, iconButtonVariants |
Names people reach for that are not here: LoadingState, PageHeader,
StatCard, NavItem, SectionHeader, FormField. Build those from the
primitives above and plain JSX.
cn is the class merger (clsx plus tailwind-merge); use it anywhere you
compose conditional classes, so a later class actually overrides an earlier one.
Component variants
Section titled “Component variants”The variant and size values below are the complete sets. A value outside them type-checks in some cases but renders nothing, so copy from here.
Button — variant: default, destructive, outline, secondary,
ghost, link, success, warning. size: default, md (a synonym for
default), sm, xs, lg, icon, icon-sm, icon-xs, menu.
IconButton — variant: ghost, subtle, outline, primary,
destructive. size: sm, md, lg. Always pass aria-label; there is no
text to read.
Badge — variant: default, secondary, destructive, outline,
success, warning, info.
Input, Textarea, Select — no variants at all. Size and shape come from
className. In particular there is no <Input size="sm">.
DialogContent portals itself and renders its own overlay, so do not wrap it in
DialogPortal or DialogOverlay. SelectContent does the same.
Tokens, not colors
Section titled “Tokens, not colors”Every color, radius, shadow, and duration in the kit comes from a CSS custom property that the active theme redefines. You reach them through Tailwind utilities, and the rule is absolute: never write a color literal.
// Correct — follows every theme<div className="bg-card text-card-foreground border border-border rounded-lg p-4">
// Wrong — unreadable the moment the user picks a light theme<div style={{ background: '#1f2937', color: '#e5e7eb' }}><div className="bg-slate-900 text-slate-100">The token-backed color utilities:
| Utility | Use for |
|---|---|
bg-background / text-foreground |
The page surface and its default text |
bg-card / text-card-foreground |
A raised panel, list, or card. bg-surface is an alias of bg-card |
bg-popover / text-popover-foreground |
Floating surfaces |
bg-primary / text-primary-foreground |
The one accent action |
bg-secondary / text-secondary-foreground |
A quiet filled surface |
bg-muted / text-muted-foreground |
De-emphasized surface and secondary text |
bg-accent / text-accent-foreground |
Hover and selected states |
border-border, ring-ring |
Borders and focus rings |
bg-destructive / text-destructive-foreground |
Danger |
bg-success, bg-warning, bg-info (+ -foreground) |
Semantic states |
bg-overlay-1, bg-overlay-2, bg-overlay-3 |
Translucent hover and pressed tints, in increasing strength |
text-fg, text-fg-secondary, text-fg-tertiary |
Foreground at full, 60%, and 45% |
bg-chart-1 … bg-chart-5 |
Categorical series |
The non-color scales are also overridden, and not to Tailwind’s defaults —
text-base is 13px, text-sm is 12px, rounded-md is 8px, rounded-lg is
10px. Arriving with stock-Tailwind muscle memory will make a page a size too big.
The type scale runs text-3xs, text-2xs, text-xs, text-sm, text-base,
text-md, text-lg, text-xl, text-2xl, text-3xl, text-4xl, with the
semantic aliases text-heading, text-subheading, text-body, text-caption,
text-code. Radii run rounded-xs through rounded-2xl plus rounded-full.
Shadows are shadow-surface, shadow-elevated, shadow-overlay,
shadow-modal, shadow-glow-accent. Motion is duration-fast (120ms),
duration-normal (200ms), duration-slow (300ms), with
ease-smooth-out, ease-smooth-in, ease-material.
Themes
Section titled “Themes”The user picks a theme, and the host writes it as a data-theme attribute on the
document’s root element. There are thirteen built-ins:
dark (the default), charcoal, midnight, daylight, ember, solar,
forest, olive, blossom, neon, lavender, crimson, meadow — plus
whatever custom themes the user has saved.
Five of them are light themes. That is the practical reason a hardcoded grey is a
bug and not a style preference: #1f2937 text is invisible on solar.
Two mechanical consequences:
Never use Tailwind’s dark: variant. It compiles, but it keys off the
operating system’s prefers-color-scheme while WAMP themes key off
data-theme. It will fire on the wrong signal. The tokens already re-point per
theme, so one set of classes covers all thirteen.
Tokens are bare HSL triplets, so inline CSS needs the hsl() wrapper.
color: var(--foreground) evaluates to 220 6% 96%, which is not a color, and
the declaration is dropped. Write color: hsl(var(--foreground)) — or better,
use the utility class.
Restyling your own surface
Section titled “Restyling your own surface”Your page renders inside a host container that paints --background and carries
data-extension-id="<your-extension-id>". That container sits between body and
your page, so rules on body or html compile fine and have no visible
effect — a successful-looking edit that changes nothing.
To retint your whole app, override tokens on that scope hook:
[data-extension-id="my-crm"] { --background: 220 12% 8%; --primary: 265 70% 60%;}Everything inside that reads the token follows, including the kit’s components.
Override the inputs — --background-{h,s,l}, --foreground-{h,s,l},
--primary, --primary-foreground — rather than the derived surfaces, since
borders, overlays, and muted text are all computed from those.
Layout
Section titled “Layout”A docked page renders at fluid width inside the workspace pane, typically
500–1200px depending on what else the user has open. Never assume a fixed width.
Make the root h-full and a flex container:
<div className="h-full flex flex-col bg-background text-foreground"> <header className="flex items-center justify-between border-b border-border p-3"> <h1 className="text-sm font-semibold">My app</h1> <Button size="sm">Action</Button> </header> <ScrollArea className="flex-1"> <div className="p-4 space-y-4">{/* body */}</div> </ScrollArea></div>A page declared "presentation": "app" takes over the window, and AppShell
gives it the matching chrome:
import { AppShell } from '@wamp/ui';
function MyApp() { return ( <AppShell> <AppShell.TitleBar> <AppShell.BackButton /> <AppShell.Title>My app</AppShell.Title> </AppShell.TitleBar> <AppShell.Content>{/* body */}</AppShell.Content> </AppShell> );}AppShell.BackButton returns the user to the previous docked view. Pass onBack
to intercept it — for instance to close an open modal first.
A complete page
Section titled “A complete page”import { useState } from 'react';import { AppShell, Badge, Button, Card, CardContent, CardHeader, CardTitle, Dialog, DialogContent, DialogHeader, DialogTitle, EmptyState, Input, Label, cn,} from '@wamp/ui';import { ListChecks, Plus } from 'lucide-react';
type Item = { id: number; title: string; done: boolean };
function ChecklistPage() { const [items, setItems] = useState<Item[]>([]); const [adding, setAdding] = useState(false); const [title, setTitle] = useState('');
const add = () => { if (!title.trim()) return; setItems((prev) => [...prev, { id: Date.now(), title: title.trim(), done: false }]); setTitle(''); setAdding(false); };
return ( <AppShell> <AppShell.TitleBar> <AppShell.BackButton /> <AppShell.Title> <span className="flex items-center gap-2 text-sm font-semibold"> <ListChecks className="w-4 h-4" /> Checklist </span> </AppShell.Title> <div className="ml-auto"> <Button size="sm" onClick={() => setAdding(true)}> <Plus className="w-4 h-4 mr-1.5" /> New item </Button> </div> </AppShell.TitleBar>
<AppShell.Content className="overflow-y-auto"> <div className="mx-auto max-w-3xl px-6 py-8"> {items.length === 0 ? ( <EmptyState icon={<ListChecks className="w-8 h-8" />} title="Nothing here yet" description="Add an item to get started." action={<Button onClick={() => setAdding(true)}>New item</Button>} /> ) : ( <Card> <CardHeader> <CardTitle>{items.filter((i) => !i.done).length} open</CardTitle> </CardHeader> <CardContent className="p-0"> <ul className="divide-y divide-border"> {items.map((item) => ( <li key={item.id} className="flex items-center gap-3 px-4 py-3 hover:bg-overlay-1 transition-colors" > <button onClick={() => setItems((prev) => prev.map((i) => (i.id === item.id ? { ...i, done: !i.done } : i)), ) } aria-label={item.done ? 'Mark as open' : 'Mark as done'} > <Badge variant={item.done ? 'success' : 'outline'}> {item.done ? 'done' : 'open'} </Badge> </button> <span className={cn( 'truncate', item.done && 'text-muted-foreground line-through', )} > {item.title} </span> </li> ))} </ul> </CardContent> </Card> )} </div> </AppShell.Content>
<Dialog open={adding} onOpenChange={setAdding}> <DialogContent> <DialogHeader> <DialogTitle>New item</DialogTitle> </DialogHeader> <div className="space-y-1.5"> <Label htmlFor="item-title">Title</Label> <Input id="item-title" value={title} onChange={(e) => setTitle(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && add()} placeholder="What needs doing?" autoFocus /> </div> <div className="flex justify-end gap-2 pt-2"> <Button variant="ghost" onClick={() => setAdding(false)}> Cancel </Button> <Button onClick={add} disabled={!title.trim()}> Add </Button> </div> </DialogContent> </Dialog> </AppShell> );}
export const views = { checklist: ChecklistPage };export default ChecklistPage;Every color in it is a token, every control is a kit component, and it renders correctly on all thirteen themes without a single conditional.
Checklist before you ship a page
Section titled “Checklist before you ship a page”- No color literals — no hex, no
rgb(), nobg-slate-*. - No invented CSS variables. If
var(--color-surface)looks plausible, check the token names above; a name that does not exist fails silently at paint time. - No
dark:variants. - No raw
<button>,<input>, or<select>where the kit has a primitive — those are the elements that stop matching the theme first. - No
bodyorhtmlrules; use thedata-extension-idscope hook. - Root is
h-fulland flexes; nothing assumes a fixed width. - Every icon-only control has an
aria-label.
- Pages and windows — where a page mounts and what
presentation: 'app'changes. - Typed data —
useDataQueryand the store behind it. - Agents and skills —
useAISession, for a page that talks to a model.