Plan
Plan card with streaming title and description and collapsible steps.
import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger,} from "@/components/ai/plan";import { Button } from "@/components/ui/button";
const steps = [ "1. Quote all four legs on Jupiter", "2. Ask for approval", "3. Send one transaction and verify weights",];
function Example({ streaming }: { streaming?: boolean }) { return ( <Plan isStreaming={streaming} defaultOpen className="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>{streaming ? "Planning…" : "Rebalance MAG4"}</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> {steps.map((s) => ( <p key={s}>{s}</p> ))} </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost"> Edit plan </Button> </PlanFooter> </Plan> );}
export default function Demo() { return ( <div className="flex flex-col gap-4"> <Example /> <Example streaming /> </div> );}<script setup lang="ts">import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger,} from "@/components/ai/plan";import { Button } from "@/components/ui/button";
const steps = [ "1. Quote all four legs on Jupiter", "2. Ask for approval", "3. Send one transaction and verify weights",];</script>
<template> <div class="flex flex-col gap-4"> <Plan v-for="streaming in [false, true]" :key="String(streaming)" :is-streaming="streaming" default-open class="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>{{ streaming ? "Planning…" : "Rebalance MAG4" }}</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> <p v-for="s in steps" :key="s">{{ s }}</p> </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost">Edit plan</Button> </PlanFooter> </Plan> </div></template><script lang="ts"> import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger, } from "$lib/components/ai/plan"; import { Button } from "$lib/components/ui/button";
const steps = [ "1. Quote all four legs on Jupiter", "2. Ask for approval", "3. Send one transaction and verify weights", ];</script>
<div class="flex flex-col gap-4"> {#each [false, true] as streaming (streaming)} <Plan isStreaming={streaming} open class="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>{streaming ? "Planning…" : "Rebalance MAG4"}</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> {#each steps as s (s)} <p>{s}</p> {/each} </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost">Edit plan</Button> </PlanFooter> </Plan> {/each}</div>Built on card, button, collapsible and ai-shimmer.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-planpnpm dlx shadcn@latest add @edmi-ui/ai-planyarn dlx shadcn@latest add @edmi-ui/ai-planbunx shadcn@latest add @edmi-ui/ai-planPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,card,collapsible,ai-shimmer,elevation.Copy 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-planpnpm dlx shadcn-vue@latest add @edmi-ui/ai-planyarn dlx shadcn-vue@latest add @edmi-ui/ai-planbunx shadcn-vue@latest add @edmi-ui/ai-planPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vue reka-uiTerminal window pnpm add @lucide/vue reka-uiTerminal window yarn add @lucide/vue reka-uiTerminal window bun add @lucide/vue reka-uiAdd the Edmi components this one builds on:
button,card,collapsible,ai-shimmer,elevation.Copy 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-plan.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-plan.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-plan.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-plan.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,card,collapsible,ai-shimmer,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger,} from "@/components/ai/plan";import { Button } from "@/components/ui/button";
<Plan isStreaming={streaming} defaultOpen className="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>{streaming ? "Planning…" : "Rebalance MAG4"}</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost"> Edit plan </Button> </PlanFooter></Plan><script setup lang="ts">import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger,} from "@/components/ai/plan";import { Button } from "@/components/ui/button";</script>
<template> <div class="flex flex-col gap-4"> <Plan v-for="streaming in [false, true]" :key="String(streaming)" :is-streaming="streaming" default-open class="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>{{ streaming ? "Planning…" : "Rebalance MAG4" }}</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> <p :key="s">{{ s }}</p> </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost">Edit plan</Button> </PlanFooter> </Plan> </div></template><script lang="ts"> import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger, } from "$lib/components/ai/plan"; import { Button } from "$lib/components/ui/button";</script>
<div class="flex flex-col gap-4"> {#each [false, true] as streaming (streaming)} <Plan isStreaming={streaming} open class="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>{streaming ? "Planning…" : "Rebalance MAG4"}</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> {#each steps as s (s)} <p>{s}</p> {/each} </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost">Edit plan</Button> </PlanFooter> </Plan> {/each}</div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps } from "react";import { createContext, useContext, useMemo } from "react";import { CaretUpDownIcon } from "@phosphor-icons/react";import { Shimmer } from "@/registry/edmi/components/ai/shimmer";import { Button } from "@/registry/edmi/ui/button";import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/registry/edmi/ui/card";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";import type { Elevation } from "@/registry/edmi/ui/elevation";
interface PlanContextValue { isStreaming: boolean;}
const PlanContext = createContext<PlanContextValue | null>(null);
const usePlan = () => { const context = useContext(PlanContext); if (!context) { throw new Error("Plan components must be used within Plan"); } return context;};
export type PlanProps = Omit<ComponentProps<typeof Collapsible>, "render"> & { isStreaming?: boolean; /** ✦ depth of the card (sunken -1, flat 0, raised +1, floating +2). */ elevation?: Elevation;};
export const Plan = ({ className, isStreaming = false, elevation, children, ...props}: PlanProps) => { const contextValue = useMemo(() => ({ isStreaming }), [isStreaming]);
return ( <PlanContext.Provider value={contextValue}> <Collapsible data-slot="ai-plan" render={ <Card className={cn("gap-0 py-0 [--card-spacing:16px]", className)} elevation={elevation} /> } {...props} > {children} </Collapsible> </PlanContext.Provider> );};
export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => ( <CardHeader className={cn("flex items-start justify-between gap-2.5 py-3.5", className)} data-slot="ai-plan-header" {...props} />);
export type PlanTitleProps = Omit< ComponentProps<typeof CardTitle>, "children"> & { children: string;};
export const PlanTitle = ({ className, children, ...props}: PlanTitleProps) => { const { isStreaming } = usePlan();
return ( <CardTitle className={cn("text-[15px]", className)} data-slot="ai-plan-title" {...props} > {isStreaming ? <Shimmer as="span">{children}</Shimmer> : children} </CardTitle> );};
export type PlanDescriptionProps = Omit< ComponentProps<typeof CardDescription>, "children"> & { children: string;};
export const PlanDescription = ({ className, children, ...props}: PlanDescriptionProps) => { const { isStreaming } = usePlan();
return ( <CardDescription className={cn("text-xs text-balance", className)} data-slot="ai-plan-description" {...props} > {isStreaming ? <Shimmer as="span">{children}</Shimmer> : children} </CardDescription> );};
export type PlanActionProps = ComponentProps<typeof CardAction>;
export const PlanAction = (props: PlanActionProps) => ( <CardAction data-slot="ai-plan-action" {...props} />);
export type PlanContentProps = ComponentProps<typeof CardContent>;
export const PlanContent = ({ className, ...props }: PlanContentProps) => ( <CollapsibleContent className="h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0"> <CardContent className={cn( "pb-3.5 text-[13.5px] leading-[1.7] text-foreground-2", className, )} data-slot="ai-plan-content" {...props} /> </CollapsibleContent>);
export type PlanFooterProps = ComponentProps<typeof CardFooter>;
export const PlanFooter = ({ className, ...props }: PlanFooterProps) => ( <CardFooter className={cn("px-4 py-2.5", className)} data-slot="ai-plan-footer" {...props} />);
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => ( <CollapsibleTrigger data-slot="ai-plan-trigger" render={ <Button className={cn("size-8", className)} size="icon-sm" variant="ghost" /> } {...props} > <CaretUpDownIcon className="size-4" /> <span className="sr-only">Toggle plan</span> </CollapsibleTrigger>);<script setup lang="ts">import type { CollapsibleRootProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { computed, provide } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Card } from "@/registry/edmi/ui/card"import { Collapsible } from "@/registry/edmi/ui/collapsible"import type { Elevation } from "@/registry/edmi/ui/elevation"import { PlanKey } from "./context"
interface PlanProps extends /* @vue-ignore */ CollapsibleRootProps { class?: HTMLAttributes["class"] isStreaming?: boolean /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation}
const props = withDefaults(defineProps<PlanProps>(), { isStreaming: false, elevation: undefined,})
provide(PlanKey, { isStreaming: computed(() => props.isStreaming) })</script>
<template> <!-- Collapsible props (open, defaultOpen, update:open) fall through as attrs. --> <Collapsible as-child data-slot="ai-plan"> <Card :elevation="props.elevation" :class="cn('gap-0 py-0 [--card-spacing:16px]', props.class)" > <slot /> </Card> </Collapsible></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { CardAction } from "@/registry/edmi/ui/card"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CardAction data-slot="ai-plan-action" :class="props.class"> <slot /> </CardAction></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CardContent } from "@/registry/edmi/ui/card"import { CollapsibleContent } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleContent class="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"> <CardContent data-slot="ai-plan-content" :class="cn('pb-3.5 text-[13.5px] leading-[1.7] text-foreground-2', props.class)" > <slot /> </CardContent> </CollapsibleContent></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed, useSlots } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Shimmer } from "@/registry/edmi/components/ai/shimmer"import { CardDescription } from "@/registry/edmi/ui/card"import { slotText, usePlanContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { isStreaming } = usePlanContext()const slots = useSlots()const text = computed(() => slotText(slots.default?.()))</script>
<template> <CardDescription data-slot="ai-plan-description" :class="cn('text-xs text-balance', props.class)"> <Shimmer v-if="isStreaming" as="span"> {{ text }} </Shimmer> <slot v-else /> </CardDescription></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CardFooter } from "@/registry/edmi/ui/card"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CardFooter data-slot="ai-plan-footer" :class="cn('px-4 py-2.5', props.class)"> <slot /> </CardFooter></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CardHeader } from "@/registry/edmi/ui/card"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CardHeader data-slot="ai-plan-header" :class="cn('flex items-start justify-between gap-2.5 py-3.5', props.class)"> <slot /> </CardHeader></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed, useSlots } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Shimmer } from "@/registry/edmi/components/ai/shimmer"import { CardTitle } from "@/registry/edmi/ui/card"import { slotText, usePlanContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { isStreaming } = usePlanContext()const slots = useSlots()const text = computed(() => slotText(slots.default?.()))</script>
<template> <CardTitle data-slot="ai-plan-title" :class="cn('text-[15px]', props.class)"> <Shimmer v-if="isStreaming" as="span"> {{ text }} </Shimmer> <slot v-else /> </CardTitle></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { ChevronsUpDownIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleTrigger as-child> <Button data-slot="ai-plan-trigger" variant="ghost" size="icon-sm" :class="cn('size-8 data-[state=open]:bg-transparent hover:data-[state=open]:bg-accent', props.class)" > <slot> <ChevronsUpDownIcon class="size-4" /> <span class="sr-only">Toggle plan</span> </slot> </Button> </CollapsibleTrigger></template>import type { InjectionKey, Ref } from "vue";import { inject } from "vue";
export interface PlanContextValue { isStreaming: Ref<boolean>;}
export const PlanKey: InjectionKey<PlanContextValue> = Symbol("PlanContext");
export function usePlanContext() { const ctx = inject(PlanKey); if (!ctx) throw new Error("Plan components must be used within <Plan>"); return ctx;}
/** Plain text of a slot (string children only); the shimmer needs it to size its highlight. */export function slotText(nodes: unknown): string { if (!Array.isArray(nodes)) return ""; let text = ""; for (const node of nodes as Array<{ children?: unknown }>) { if (typeof node.children === "string") text += node.children; } return text;}export * from "./context";export { default as Plan } from "./Plan.vue";export { default as PlanAction } from "./PlanAction.vue";export { default as PlanContent } from "./PlanContent.vue";export { default as PlanDescription } from "./PlanDescription.vue";export { default as PlanFooter } from "./PlanFooter.vue";export { default as PlanHeader } from "./PlanHeader.vue";export { default as PlanTitle } from "./PlanTitle.vue";export { default as PlanTrigger } from "./PlanTrigger.vue";import Root from "./plan.svelte";import Action from "./plan-action.svelte";import Content from "./plan-content.svelte";import Description from "./plan-description.svelte";import Footer from "./plan-footer.svelte";import Header from "./plan-header.svelte";import Title from "./plan-title.svelte";import Trigger from "./plan-trigger.svelte";
export * from "./use-plan.svelte.js";export { Action, Action as PlanAction, Content, Content as PlanContent, Description, Description as PlanDescription, Footer, Footer as PlanFooter, Header, Header as PlanHeader, Root, // Root as Plan, Title, Title as PlanTitle, Trigger, Trigger as PlanTrigger,};<script lang="ts"> import { CardAction } from "$lib/registry/ui/card/index.js"; import type { ComponentProps } from "svelte";
let { children, ...restProps }: ComponentProps<typeof CardAction> = $props();</script>
<CardAction data-slot="ai-plan-action" {...restProps}> {@render children?.()}</CardAction><script lang="ts"> import { cn } from "$lib/utils.js"; import { CardContent } from "$lib/registry/ui/card/index.js"; import { CollapsibleContent } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CardContent> = $props();</script>
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"> <CardContent data-slot="ai-plan-content" class={cn("pb-3.5 text-[13.5px] leading-[1.7] text-foreground-2", className)} {...restProps} > {@render children?.()} </CardContent></CollapsibleContent><script lang="ts"> import { cn } from "$lib/utils.js"; import { CardDescription } from "$lib/registry/ui/card/index.js"; import type { ComponentProps } from "svelte"; import { Shimmer } from "../shimmer/index.js"; import { usePlan } from "./use-plan.svelte.js";
let { class: className, children, ...restProps }: ComponentProps<typeof CardDescription> = $props();
const plan = usePlan();</script>
<CardDescription data-slot="ai-plan-description" class={cn("text-xs text-balance", className)} {...restProps}> {#if plan.isStreaming} <Shimmer as="span">{@render children?.()}</Shimmer> {:else} {@render children?.()} {/if}</CardDescription><script lang="ts"> import { cn } from "$lib/utils.js"; import { CardFooter } from "$lib/registry/ui/card/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CardFooter> = $props();</script>
<CardFooter data-slot="ai-plan-footer" class={cn("px-4 py-2.5", className)} {...restProps}> {@render children?.()}</CardFooter><script lang="ts"> import { cn } from "$lib/utils.js"; import { CardHeader } from "$lib/registry/ui/card/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CardHeader> = $props();</script>
<CardHeader data-slot="ai-plan-header" class={cn("flex items-start justify-between gap-2.5 py-3.5", className)} {...restProps}> {@render children?.()}</CardHeader><script lang="ts"> import { cn } from "$lib/utils.js"; import { CardTitle } from "$lib/registry/ui/card/index.js"; import type { ComponentProps } from "svelte"; import { Shimmer } from "../shimmer/index.js"; import { usePlan } from "./use-plan.svelte.js";
let { class: className, children, ...restProps }: ComponentProps<typeof CardTitle> = $props();
const plan = usePlan();</script>
<CardTitle data-slot="ai-plan-title" class={cn("text-[15px]", className)} {...restProps}> {#if plan.isStreaming} <Shimmer as="span">{@render children?.()}</Shimmer> {:else} {@render children?.()} {/if}</CardTitle><script lang="ts"> import CaretUpDownIcon from "phosphor-svelte/lib/CaretUpDown"; import { cn } from "$lib/utils.js"; import { Button } from "$lib/registry/ui/button/index.js"; import { CollapsibleTrigger } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CollapsibleTrigger> = $props();</script>
<CollapsibleTrigger data-slot="ai-plan-trigger" {...restProps}> {#snippet child({ props })} <Button {...props} variant="ghost" size="icon-sm" class={cn("size-8 data-[state=open]:bg-transparent hover:data-[state=open]:bg-accent", className)} > {#if children} {@render children()} {:else} <CaretUpDownIcon class="size-4" /> <span class="sr-only">Toggle plan</span> {/if} </Button> {/snippet}</CollapsibleTrigger><script lang="ts"> import { cn } from "$lib/utils.js"; import { Card } from "$lib/registry/ui/card/index.js"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { Collapsible } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte"; import { setPlanContext } from "./use-plan.svelte.js";
let { class: className, isStreaming = false, elevation = "auto", open = $bindable(false), children, ...restProps }: Omit<ComponentProps<typeof Collapsible>, "child"> & { isStreaming?: boolean; /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; } = $props();
setPlanContext({ get isStreaming() { return isStreaming; }, });</script>
<Collapsible bind:open data-slot="ai-plan" {...restProps}> {#snippet child({ props })} <Card {...props} {elevation} class={cn("gap-0 py-0 [--card-spacing:16px]", className)}> {@render children?.()} </Card> {/snippet}</Collapsible>import { getContext, setContext } from "svelte";
/** Getter object so consumers stay reactive. */export interface PlanContextValue { readonly isStreaming: boolean;}
const KEY = Symbol("ai-plan");
export function setPlanContext(value: PlanContextValue) { return setContext(KEY, value);}
export function usePlan(): PlanContextValue { const ctx = getContext<PlanContextValue | undefined>(KEY); if (!ctx) throw new Error("Plan components must be used within <Plan>"); return ctx;}Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
The plan card are flat by default and take the surface level in layered mode (raised). sunken (-1) is a soft inset, raised (+1) a bevel and floating (+2) a bevel with a soft drop.
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger,} from "@/components/ai/plan";import { Button } from "@/components/ui/button";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex w-full max-w-2xl flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Plan elevation={value} defaultOpen className="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>Rebalance MAG4</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> <p>1. Quote all four legs on Jupiter</p> <p>2. Ask for approval</p> </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost"> Edit plan </Button> </PlanFooter> </Plan> </div> ))} </div> );}<script setup lang="ts">import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger,} from "@/components/ai/plan";import { Button } from "@/components/ui/button";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex w-full max-w-2xl flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <Plan :elevation="level.value" default-open class="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>Rebalance MAG4</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> <p>1. Quote all four legs on Jupiter</p> <p>2. Ask for approval</p> </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost">Edit plan</Button> </PlanFooter> </Plan> </div> </div></template><script lang="ts"> import { Plan, PlanAction, PlanContent, PlanDescription, PlanFooter, PlanHeader, PlanTitle, PlanTrigger, } from "$lib/components/ai/plan"; import { Button } from "$lib/components/ui/button";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex w-full max-w-2xl flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <Plan elevation={level.value} open class="w-full max-w-md"> <PlanHeader> <div> <PlanTitle>Rebalance MAG4</PlanTitle> <PlanDescription>3 steps · about 1 minute</PlanDescription> </div> <PlanAction> <PlanTrigger /> </PlanAction> </PlanHeader> <PlanContent> <p>1. Quote all four legs on Jupiter</p> <p>2. Ask for approval</p> </PlanContent> <PlanFooter> <Button size="sm">Start</Button> <Button size="sm" variant="ghost">Edit plan</Button> </PlanFooter> </Plan> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
| Plan | isStreaming |
boolean (title and description shimmer) |
false |
| Plan | elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
| Plan | open, defaultOpen, onOpenChange |
collapsible state | closed |
| PlanTitle, PlanDescription | children |
string |
|
| PlanTrigger | Collapsible trigger | ghost icon button | |
| PlanFooter | card footer |
✦ Edmi additions
Section titled “✦ Edmi additions”elevation✦ goes to the Card; the footerButtons take their ownelevation.- The footer sits on the shell colour, like every Card footer in Edmi.