Interface kit
Build extension UI from @wamp/ui components and theme tokens. The host supplies
the runtime module; the SDK supplies authoring types. Desktop also exposes
composites tied to its own state, so choose shared primitives for Cloud panels.
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”The table groups the established Desktop authoring surface. The SDK typecheck
is authoritative for a particular export; a runtime export alone does not mean
it is available to typed extensions. The Cloud host provides shared primitives
and Markdown, but not Desktop composites such as ModelPicker,
useDataQuery, or usePageParams. An unsupported Cloud import produces a
named placeholder rather than the Desktop feature. Portable views can use
pluginAPI directly and manage their own React state.
| Group | Exports |
|---|---|
| Buttons and actions | Button, IconButton, Chip, SegmentedControl |
| Inputs | Input, Textarea, SearchInput, Label, Checkbox, Switch, EffortSlider, 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, ListSkeleton, SectionLabel, Toast (+ ToastProvider, ToastViewport, ToastTitle, ToastDescription, ToastClose, ToastAction) |
| App chrome | AppShell with AppShell.TitleBar, AppShell.Title, AppShell.Content |
| Content | Markdown, TextOutput, WebPreview |
| AI | ModelPicker, ProviderIcon |
| Tool cards | ToolCard, ToolCardHeader, ToolCardError, ToolBody, ToolDetail, ToolChain, CompactToolRow, StatusIndicator, DiffLinesView, ScreenshotCard, ScreenshotImage |
| Hooks | useDataQuery, useToggleState, usePageParams |
| Utilities | cn, renderIcon, formatRelative, 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, warning. size: default, md (a synonym for
default), sm, xs, lg, icon, icon-sm, icon-xs.
IconButton — variant: ghost, subtle, outline, primary,
destructive, solid. size: sm, md, lg, chrome (a pane-header
control: the sm box with a smaller 22px hover fill centred in it). Always
pass aria-label; there is no text to read.
Badge — variant: default, secondary, destructive, outline,
success, warning, info.
Input, Textarea, Select — no variant. Input size is sm
(h-7) or md (h-8); the default is sm. Textarea and Select take
their size from className.
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-ui-micro, text-xs, text-sm, text-base,
text-md, text-lg, text-xl, text-2xl, text-3xl, text-4xl, with the
semantic roles text-ui-micro, text-ui-caption, text-ui-label, text-ui-body,
text-content-body, and text-content-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 eight built-ins:
dark (the default), light, charcoal, paper, midnight, ember, dim,
forest — plus whatever custom themes the user has saved.
Two of them are light themes (light, paper). That is the practical reason a
hardcoded grey is a bug and not a style preference: #1f2937 text is invisible
on paper.
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 every built-in.
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.Title>My app</AppShell.Title> </AppShell.TitleBar> <AppShell.Content>{/* body */}</AppShell.Content> </AppShell> );}Leave an app page with pluginAPI.ui.exitAppMode(), or close the window.
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.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;This page keeps items in React state; closing it loses the list. Use typed data to persist them. Check the finished layout in both light and dark themes before distributing it.
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 — ship a configured agent a
page can run with
pluginAPI.agents.delegate.