Adaptive Card Designer
Visual drag-and-drop designer for Microsoft Adaptive Cards 1.5 / 1.6 payloads. Three-pane editor (toolbox · canvas · structure / properties) with paired JSON editors, live preview, undo / redo, and theme / width controls — backed by the in-repo Adaptive Card renderer.
Installation
pnpm dlx @docyrus/cli add @docyrus/ui-adaptive-card-designernpx shadcn@latest add button dropdown-menu input label select switch textarea tooltippnpm add @dnd-kit/core @monaco-editor/reactThe designer renders its canvas with the AdaptiveCard renderer, which is installed automatically as a registry dependency.
Overview
AdaptiveCardDesigner is a full visual authoring surface for Adaptive Cards payloads. Where AdaptiveCard only renders a payload, the designer lets users build one — dragging elements from a toolbox, reordering and reparenting them in a structure tree, editing element properties in a side panel, and watching the result render live. The raw payload and the templating sample-data are both editable as JSON at the bottom, and the two views stay in sync.
Typical use cases:
- Internal card authoring — a no-code surface for ops / support teams to compose Teams-style cards and agent UI without hand-writing JSON.
- Template tooling — author the payload that an LLM agent or automation later fills with
${...}bindings, previewed against sample data. - Embedded editors — drop the designer into an automation-node config or a CMS, read the emitted payload back via
onChange.
The designer works on its own normalized node tree internally (stable per-node ids, uniform child slots) and serializes back to a standard AdaptiveCardPayload on every edit.
Layout
The designer is a three-pane layout with a toolbar above and paired JSON editors below:
| Region | Purpose |
|---|---|
| Toolbar | New / undo / redo / copy, theme (light / dark / auto), canvas width (standard / wide / full), preview toggle. Buttons are individually hideable via hideToolbarButtons. |
| Toolbox (left) | Draggable element / input / action types grouped into elements, inputs, actions, advanced. Append your own via extraToolboxItems. |
| Canvas (center) | Live render of the card via <AdaptiveCard>. Click an element to select it; drop toolbox items directly onto it. |
| Structure (right, top) | Collapsible tree of the card. Reorder / reparent by drag, select to edit. |
| Properties (right, bottom) | Type-aware property editors for the selected node. |
| Payload editor (bottom) | The live AdaptiveCardPayload JSON (Monaco). |
| Sample-data editor (bottom) | The templating data JSON used to resolve ${...} bindings in preview. |
Usage
Uncontrolled
import { AdaptiveCardDesigner } from '@docyrus/ui/components/adaptive-card-designer';
import { type AdaptiveCardPayload } from '@docyrus/ui/components/adaptive-card';
const starter: AdaptiveCardPayload = {
type: 'AdaptiveCard',
version: '1.5',
body: [{ type: 'TextBlock', text: 'Hello, **Adaptive Cards**!', weight: 'bolder' }]
};
<AdaptiveCardDesigner
defaultPayload={starter}
defaultSampleData={{ user: { name: 'Alice' } }}
onChange={({ payload, sampleData }) => console.log(payload, sampleData)} />Controlled
Pass payload / sampleData to drive the designer from outside. onChange fires after every edit:
const [payload, setPayload] = useState<AdaptiveCardPayload>(starter);
const [data, setData] = useState<unknown>({});
<AdaptiveCardDesigner
payload={payload}
sampleData={data}
onChange={next => { setPayload(next.payload); setData(next.sampleData); }} />Read-only inspection
readOnly disables every mutation — toolbox drag, drop zones, property edits, JSON edits, undo / redo / new, and keyboard shortcuts. Theme / width / preview / copy stay active, and selection remains enabled so users can click through the tree and inspect properties:
<AdaptiveCardDesigner payload={payload} readOnly />Custom toolbox items
Append your own draggable types alongside the built-ins. Each item supplies a factory that returns a fresh node on every drop:
import { Sparkles } from 'lucide-react';
import { type ToolboxItem } from '@docyrus/ui/components/adaptive-card-designer';
const extras: ToolboxItem[] = [
{
id: 'Docyrus.UserChip',
type: 'Docyrus.UserChip',
label: 'User Chip',
icon: Sparkles,
group: 'advanced',
keywords: ['user', 'chip', 'avatar'],
factory: () => ({
__designerId: crypto.randomUUID(),
type: 'Docyrus.UserChip',
props: { userId: '' },
slots: {}
})
}
];
<AdaptiveCardDesigner
extraToolboxItems={extras}
customElements={{ 'Docyrus.UserChip': ({ element }) => <UserChip id={element.userId} /> }} />Pair extraToolboxItems (authoring) with customElements (rendering) so the canvas knows how to draw the type you just added. customElements and hostConfig are forwarded straight to the inner <AdaptiveCard>.
Trimming the toolbar
<AdaptiveCardDesigner hideToolbarButtons={['new', 'copy']} />API Reference
AdaptiveCardDesignerProps
| Prop | Type | Default | Description |
|---|---|---|---|
payload | AdaptiveCardPayload | — | Current card payload (controlled). |
defaultPayload | AdaptiveCardPayload | — | Initial payload when uncontrolled. |
sampleData | unknown | — | Templating data resolved into the preview canvas (controlled). |
defaultSampleData | unknown | — | Initial sample data when uncontrolled. |
onChange | (next: { payload: AdaptiveCardPayload; sampleData: unknown }) => void | — | Fires after every payload or sample-data edit. |
hostConfig | AdaptiveCardHostConfigOverride | — | Forwarded to the inner <AdaptiveCard hostConfig />. |
customElements | Record<string, ElementRenderer> | {} | Forwarded to the inner <AdaptiveCard customElements />. Pair with extraToolboxItems to author custom types. |
defaultPreview | boolean | false | Start in preview (render-only) mode rather than edit mode. |
defaultTheme | DesignerTheme | 'light' | Initial canvas theme — 'light' | 'dark' | 'auto'. |
defaultWidth | DesignerWidth | 'standard' | Initial canvas width — 'standard' | 'wide' | 'full'. |
hideToolbarButtons | ToolbarButtonKey[] | — | Hide specific toolbar buttons — 'new' | 'theme' | 'width' | 'undo' | 'redo' | 'copy' | 'preview'. |
extraToolboxItems | ToolboxItem[] | — | Append additional toolbox items alongside the built-ins. |
height | string | '70vh' | Designer chrome height. |
className | string | — | Forwarded to the root element. |
readOnly | boolean | false | Disable all mutations. Theme / width / preview / copy and selection stay active. |
aiAssistantOpen | boolean | — | Controlled open state for the AI Assistant drawer. When provided the designer stops managing the open state internally — pair with onAiAssistantOpenChange. |
onAiAssistantOpenChange | (open: boolean) => void | — | Fired when the AI Assistant toolbar button toggles the drawer. |
renderAiAssistant | (ctx: IAdaptiveCardAiAssistantRenderContext) => ReactNode | — | Mounts a custom AI Assistant drawer body on the left of the designer. When set, the toolbar shows a Bot toggle that opens/closes the drawer; this render fn supplies the body (typically a <DocyrusAgent> or <EditorAgent> wrapper). Without it the AI button is hidden. |
aiAssistantWidth | number | 380 | Width in pixels the drawer animates to when open. |
EditorAgent integration
Pair useApplyAdaptiveCard with renderAiAssistant to let an LLM author Adaptive Card payloads. The hook owns the designer's controlled payload + sampleData and exposes an applyAdaptiveCard tool that the agent calls to commit a new payload. The tool rejects primitives / arrays / null and requires type: "AdaptiveCard" at the root so a malformed call cannot crash the designer.
import { EditorAgent } from '@docyrus/ui/components/editor-agent';
import { AdaptiveCardDesigner, useApplyAdaptiveCard } from '@docyrus/ui/components/adaptive-card-designer';
export function AdaptiveCardPlayground({ client, user, agentId }) {
const adaptiveCard = useApplyAdaptiveCard();
const [aiOpen, setAiOpen] = useState(false);
return (
<AdaptiveCardDesigner
payload={adaptiveCard.payload ?? undefined}
sampleData={adaptiveCard.sampleData}
onChange={(next) => {
adaptiveCard.setPayload(next.payload);
adaptiveCard.setSampleData(next.sampleData);
}}
aiAssistantOpen={aiOpen}
onAiAssistantOpenChange={setAiOpen}
renderAiAssistant={({ open, onClose }) => (
<EditorAgent
agentId={agentId}
client={client}
user={user}
open={open}
onClose={onClose}
clientTools={adaptiveCard.tools}
/>
)}
/>
);
}The matching backend agent must register a tool named applyAdaptiveCard whose input schema mirrors { payload: object (required), sampleData?: any, explanation?: string }.
Composition
The default AdaptiveCardDesigner is a single component that composes a provider + the panes. For custom layouts you can assemble the pieces yourself:
| Export | Purpose |
|---|---|
DesignerProvider | Holds reducer state + history. Wrap your own panel composition in it. |
useDesignerContext() | Read state + dispatch inside a custom panel. |
useApplyAdaptiveCard() | Owns the controlled payload + sampleData state and ships the applyAdaptiveCard client-side tool for <EditorAgent>. See EditorAgent integration above. |
Helpers
The component also exports its tree model utilities for advanced integrations (custom serializers, programmatic edits, validation):
| Export | Purpose |
|---|---|
cardToTree(payload) / treeToCard(node) | Convert between an AdaptiveCardPayload and the designer's DesignerNode tree. |
normalizeForRoundTrip(payload) | Normalize a payload so tree → card → tree is lossless. |
slotsFor(type) / SLOT_MAP | The slot names (items, columns, actions, …) a given element type accepts. |
isLeafType(type) | Whether a type accepts no children. |
createDefaultNode(type) / defaultChildType(type) | Factory helpers for new nodes. |
createDesignerId() | Generate a stable internal node id. |
findNode, findLocation, insertNode, removeNode, moveNode, updateNode, isAncestor, collectIds, countNodes | Tree traversal + mutation utilities. |
Type Exports
| Type | Description |
|---|---|
AdaptiveCardDesignerProps | Component props. |
DesignerProviderProps | Props for DesignerProvider. |
DesignerNode | A node in the designer's normalized tree — { __designerId, type, props, slots }. |
DesignerState / DesignerAction | Reducer state + action union. |
DesignerTheme | 'light' | 'dark' | 'auto'. |
DesignerWidth | 'standard' | 'wide' | 'full'. |
DesignerFocus | Which editor was last focused — 'canvas' | 'payload' | 'data'. |
DesignerDiagnostic | A validation diagnostic — { level, message, nodeId? }. |
HistorySnapshot | An undo / redo history entry. |
ToolboxItem / ToolboxGroup | A toolbox entry and its group key. |
ToolbarButtonKey | Keys accepted by hideToolbarButtons. |
DesignerRootType | The '__root' discriminant for the card root node. |
IAdaptiveCardAiAssistantRenderContext | Argument passed to renderAiAssistant — { open, width, onClose, payload, sampleData }. |
IUseApplyAdaptiveCardResult | Return shape of useApplyAdaptiveCard — { payload, setPayload, sampleData, setSampleData, tools }. |
Related
AdaptiveCard— the renderer the designer canvas is built on.useAdaptiveCard— the hook behind the renderer.