# Interface kit Build a page with @wamp/ui so it inherits the host's components, spacing, type scale, and every theme the user can switch to. Source: https://docs.vampikez.fun/build/ui/ 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 Import it. That is the whole setup. ```tsx ``` 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. :::caution[Do not `npm install @wamp/ui`] The kit is not published to npm. It reaches your extension from the running host at load time, and its types come from the SDK. `npm install @wamp/ui` fetches something else or fails; either way it does not give you this module. The same applies to `react`, `react-dom`, `lucide-react`, and `@wamp/plugin-api` — all provided by the host, none of them yours to bundle. ::: Icons come from `lucide-react`, which is available at runtime the same way: ```tsx ``` ## 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 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. :::caution[Use the declared props] The type declarations extensions are checked against are hand-written, and a few non-variant props drifted: - ``, ``, and `` all work at runtime but are missing from the declarations, so they fail the typecheck. Avoid them for now. - `Button` has no `asChild` API. Use its declared button props. Variant and size values are checked against the real component source automatically, so those you can trust. ::: ## 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.** ```tsx // Correct — follows every theme
// Wrong — unreadable the moment the user picks a light theme
``` 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 | :::caution[`/opacity` modifiers work on some colors and silently not others] `background`, `foreground`, `primary`, `ring`, `destructive`, `success`, `warning`, and `info` are plain HSL triplets, so `bg-primary/90` and `bg-foreground/20` work. `card`, `surface`, `popover`, `secondary`, `muted`, `accent`, `input`, and `border` are pre-composed colors that cannot take a modifier — `bg-card/50` and `border-border/70` emit nothing. For a translucent surface use the `bg-overlay-1/2/3` scale instead. ::: 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 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 Your page renders inside a host container that paints `--background` and carries `data-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: ```css [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 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: ```tsx

My app

{/* body */}
``` A page declared `"presentation": "app"` takes over the window, and `AppShell` gives it the matching chrome: ```tsx function MyApp() { return ( My app {/* body */} ); } ``` Leave an app page with `pluginAPI.ui.exitAppMode()`, or close the window. ## A complete page ```tsx // ui/index.tsx AppShell, Badge, Button, Card, CardContent, CardHeader, CardTitle, Dialog, DialogContent, DialogHeader, DialogTitle, EmptyState, Input, Label, cn, } from '@wamp/ui'; type Item = { id: number; title: string; done: boolean }; function ChecklistPage() { const [items, setItems] = useState([]); 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 ( Checklist
{items.length === 0 ? ( } title="Nothing here yet" description="Add an item to get started." action={} /> ) : ( {items.filter((i) => !i.done).length} open
    {items.map((item) => (
  • {item.title}
  • ))}
)}
New item
setTitle(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && add()} placeholder="What needs doing?" autoFocus />
); } export const views = { checklist: ChecklistPage }; export default ChecklistPage; ``` This page keeps items in React state; closing it loses the list. Use [typed data](/build/typed-data/) to persist them. Check the finished layout in both light and dark themes before distributing it. ## Checklist before you ship a page - No color literals — no hex, no `rgb()`, no `bg-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 `