Toolbar
Floating toolbar attached to a selected node.
import { Canvas } from "@/components/ai/canvas";import { Node, NodeContent, NodeDescription, NodeHeader, NodeTitle,} from "@/components/ai/node";import { Toolbar } from "@/components/ai/toolbar";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";
const nodes = [ { id: "decision", type: "decision", position: { x: 0, y: 40 }, data: {}, selected: true, },];
const nodeTypes = { decision: () => ( <> <Toolbar isVisible> <Button size="icon-xs" variant="ghost" aria-label="Settings"> <svg aria-hidden="true" fill="none" height="14" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" viewBox="0 0 24 24" width="14" > <circle cx="12" cy="12" r="3" /> <path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1" /> </svg> </Button> <Button size="icon-xs" variant="ghost" aria-label="Duplicate"> <svg aria-hidden="true" fill="none" height="14" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" viewBox="0 0 24 24" width="14" > <rect height="11" rx="2" width="11" x="9" y="9" /> <path d="M5 15V6a2 2 0 0 1 2-2h9" /> </svg> </Button> <Button size="icon-xs" variant="ghost" aria-label="Delete"> <svg aria-hidden="true" fill="none" height="14" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" viewBox="0 0 24 24" width="14" > <path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" /> </svg> </Button> </Toolbar> <Node handles={{ target: true, source: true }}> <NodeHeader> <NodeTitle>Decision</NodeTitle> <NodeDescription>drift > 2%?</NodeDescription> </NodeHeader> <NodeContent> <Badge variant="success">yes</Badge> </NodeContent> </Node> </> ),};
export default function Demo() { return ( <div className="h-64 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas edges={[]} fitViewOptions={{ maxZoom: 1, padding: 0.4 }} nodes={nodes} nodeTypes={nodeTypes} /> </div> );}<script setup lang="ts">import { CopyIcon, SettingsIcon, Trash2Icon } from "@lucide/vue";import { Canvas } from "@/components/ai/canvas";import { Node, NodeContent, NodeDescription, NodeHeader, NodeTitle } from "@/components/ai/node";import { Toolbar } from "@/components/ai/toolbar";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";
const nodes = [{ id: "decision", type: "decision", position: { x: 0, y: 40 }, data: {}, selected: true }];
// One node at zoom 1 with room around it (the default fit view zooms a lone node to 1.5x).const fit = (flow: { fitView: (o: { maxZoom: number; padding: number }) => unknown }) => setTimeout(() => flow.fitView({ maxZoom: 1, padding: 0.4 }), 50);</script>
<template> <div class="h-64 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas :nodes="nodes" :edges="[]" :fit-view-on-init="false" @init="fit"> <template #node-decision="{ selected }"> <Toolbar :is-visible="true"> <Button size="icon-xs" variant="ghost" aria-label="Settings"><SettingsIcon /></Button> <Button size="icon-xs" variant="ghost" aria-label="Duplicate"><CopyIcon /></Button> <Button size="icon-xs" variant="ghost" aria-label="Delete"><Trash2Icon /></Button> </Toolbar> <Node :handles="{ target: true, source: true }" :selected="selected"> <NodeHeader> <NodeTitle>Decision</NodeTitle> <NodeDescription>drift > 2%?</NodeDescription> </NodeHeader> <NodeContent> <Badge variant="success">yes</Badge> </NodeContent> </Node> </template> </Canvas> </div></template><script lang="ts"> import { Canvas } from "$lib/components/ai/canvas"; import Step from "./_shared/ai-workflow-step.svelte";
const nodeTypes = { step: Step };
let nodes = $state.raw([ { id: "decision", type: "step", position: { x: 0, y: 40 }, selected: true, data: { title: "Decision", description: "drift > 2%?", badges: ["yes"], toolbar: true, handles: { target: true, source: true } } }, ]); let edges = $state.raw([]);</script>
<div style="height: 256px" class="w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas bind:nodes bind:edges {nodeTypes} fitViewOptions={{ maxZoom: 1, padding: 0.4 }} /></div>A solid --popover chip with a 1px border (rule 7), shown when the node is selected.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-toolbarpnpm dlx shadcn@latest add @edmi-ui/ai-toolbaryarn dlx shadcn@latest add @edmi-ui/ai-toolbarbunx shadcn@latest add @edmi-ui/ai-toolbarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @xyflow/reactTerminal window pnpm add @xyflow/reactTerminal window yarn add @xyflow/reactTerminal window bun add @xyflow/reactCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-toolbarpnpm dlx shadcn-vue@latest add @edmi-ui/ai-toolbaryarn dlx shadcn-vue@latest add @edmi-ui/ai-toolbarbunx shadcn-vue@latest add @edmi-ui/ai-toolbarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @vue-flow/core @vue-flow/node-toolbar @vueuse/coreTerminal window pnpm add @vue-flow/core @vue-flow/node-toolbar @vueuse/coreTerminal window yarn add @vue-flow/core @vue-flow/node-toolbar @vueuse/coreTerminal window bun add @vue-flow/core @vue-flow/node-toolbar @vueuse/coreCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-toolbar.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-toolbar.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-toolbar.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-toolbar.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { Canvas } from "@/components/ai/canvas";
<div className="h-64 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas edges={[]} fitViewOptions={{ maxZoom: 1, padding: 0.4 }} /></div><script setup lang="ts">import { Canvas } from "@/components/ai/canvas";import { Node, NodeContent, NodeDescription, NodeHeader, NodeTitle,} from "@/components/ai/node";import { Toolbar } from "@/components/ai/toolbar";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";</script>
<template> <div class="h-64 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas :edges="[]" :fit-view-on-init="false"> <template #node-decision="{ selected }"> <Toolbar :is-visible="true"> <Button size="icon-xs" variant="ghost" aria-label="Settings"><SettingsIcon /></Button> <Button size="icon-xs" variant="ghost" aria-label="Duplicate"><CopyIcon /></Button> <Button size="icon-xs" variant="ghost" aria-label="Delete"><Trash2Icon /></Button> </Toolbar> <Node :handles="{ target: true, source: true }" :selected="selected"> <NodeHeader> <NodeTitle>Decision</NodeTitle> <NodeDescription>drift > 2%?</NodeDescription> </NodeHeader> <NodeContent> <Badge variant="success">yes</Badge> </NodeContent> </Node> </template> </Canvas> </div></template><script lang="ts"> import { Canvas } from "$lib/components/ai/canvas";</script>
<div style="height: 256px" class="w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas bind:nodes bind:edges fitViewOptions={{ maxZoom: 1, padding: 0.4 }} /></div>Component source
Section titled “Component source”import { NodeToolbar, Position } from "@xyflow/react";import { cn } from "cn";import type { ComponentProps } from "react";
export type ToolbarProps = ComponentProps<typeof NodeToolbar>;
/** Floating actions attached to a node (solid chip: `--popover` + 1px border). Above the node by default. */export const Toolbar = ({ className, ...props }: ToolbarProps) => ( <NodeToolbar data-slot="ai-toolbar" className={cn( "flex items-center gap-0.5 rounded-[9px] border border-border bg-popover p-[3px]", className, )} position={Position.Top} {...props} />);<script setup lang="ts">import type { HTMLAttributes } from "vue"import { Position } from "@vue-flow/core"import { NodeToolbar } from "@vue-flow/node-toolbar"import { reactiveOmit } from "@vueuse/core"import { cn } from "@/registry/edmi/lib/utils"
/** Floating actions attached to a node (solid chip: --popover + 1px border). Above the node by default. */const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] position?: Position isVisible?: boolean offset?: number}>(), { position: Position.Top, isVisible: undefined,})
const delegatedProps = reactiveOmit(props, "class")</script>
<template> <NodeToolbar data-slot="ai-toolbar" v-bind="delegatedProps" :class="cn('flex items-center gap-0.5 rounded-[9px] border border-border bg-popover p-[3px]', props.class)" > <slot /> </NodeToolbar></template>export { default as Toolbar } from "./Toolbar.vue";<script lang="ts"> import { NodeToolbar, Position, type NodeToolbarProps } from "@xyflow/svelte"; import { cn } from "$lib/utils.js";
/** Floating actions attached to a node (solid chip: --popover + 1px border). Above the node by default. */ let { class: className, position = Position.Top, children, ...restProps }: NodeToolbarProps = $props();</script>
<NodeToolbar data-slot="ai-toolbar" {position} class={cn( "flex items-center gap-0.5 rounded-[9px] border border-border bg-popover p-[3px]", className )} {...restProps}> {@render children?.()}</NodeToolbar>import Toolbar from "./toolbar.svelte";
export { Toolbar };| Part | Prop | Type | Default |
|---|---|---|---|
| Toolbar | isVisible, position, offset |
xyflow NodeToolbar props | position defaults to top |