Suggestion
Suggested prompts as pills in a scrollable row, with a card variant for the home state.
import { Suggestion, Suggestions } from "@/components/ai/suggestion";
const prompts = [ "What moved NVDAx today?", "Compare MAG4 vs SPYx", "Explain the keeper", "Show my fees",];
export default function Demo() { return ( <div className="flex w-full max-w-2xl flex-col gap-4"> <Suggestions> {prompts.map((p) => ( <Suggestion key={p} suggestion={p} onClick={() => {}} /> ))} </Suggestions> <div className="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" variant="card"> Check drift and propose trades </Suggestion> <Suggestion suggestion="Draft a post" variant="card"> Write an update for the feed </Suggestion> </div> </div> );}<script setup lang="ts">import { Suggestion, Suggestions } from "@/components/ai/suggestion";
const prompts = [ "What moved NVDAx today?", "Compare MAG4 vs SPYx", "Explain the keeper", "Show my fees",];</script>
<template> <div class="flex w-full max-w-2xl flex-col gap-4"> <Suggestions> <Suggestion v-for="p in prompts" :key="p" :suggestion="p" /> </Suggestions> <div class="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" variant="card">Check drift and propose trades</Suggestion> <Suggestion suggestion="Draft a post" variant="card">Write an update for the feed</Suggestion> </div> </div></template><script lang="ts"> import { Suggestion, Suggestions } from "$lib/components/ai/suggestion";
const prompts = [ "What moved NVDAx today?", "Compare MAG4 vs SPYx", "Explain the keeper", "Show my fees", ];</script>
<div class="flex w-full max-w-2xl flex-col gap-4"> <Suggestions> {#each prompts as p (p)} <Suggestion suggestion={p} /> {/each} </Suggestions> <div class="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" variant="card">Check drift and propose trades</Suggestion> <Suggestion suggestion="Draft a post" variant="card">Write an update for the feed</Suggestion> </div></div>Built on button (outline, pill).
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-suggestionpnpm dlx shadcn@latest add @edmi-ui/ai-suggestionyarn dlx shadcn@latest add @edmi-ui/ai-suggestionbunx shadcn@latest add @edmi-ui/ai-suggestionPick 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,scroll-area,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-suggestionpnpm dlx shadcn-vue@latest add @edmi-ui/ai-suggestionyarn dlx shadcn-vue@latest add @edmi-ui/ai-suggestionbunx shadcn-vue@latest add @edmi-ui/ai-suggestionPick 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,scroll-area,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-suggestion.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-suggestion.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-suggestion.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-suggestion.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,scroll-area,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Suggestion, Suggestions } from "@/components/ai/suggestion";
<div className="flex w-full max-w-2xl flex-col gap-4"> <Suggestions> </Suggestions> <div className="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" variant="card"> Check drift and propose trades </Suggestion> <Suggestion suggestion="Draft a post" variant="card"> Write an update for the feed </Suggestion> </div></div><script setup lang="ts">import { Suggestion, Suggestions } from "@/components/ai/suggestion";</script>
<template> <div class="flex w-full max-w-2xl flex-col gap-4"> <Suggestions> <Suggestion :key="p" :suggestion="p" /> </Suggestions> <div class="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" variant="card">Check drift and propose trades</Suggestion> <Suggestion suggestion="Draft a post" variant="card">Write an update for the feed</Suggestion> </div> </div></template><script lang="ts"> import { Suggestion, Suggestions } from "$lib/components/ai/suggestion";</script>
<div class="flex w-full max-w-2xl flex-col gap-4"> <Suggestions> {#each prompts as p (p)} <Suggestion suggestion={p} /> {/each} </Suggestions> <div class="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" variant="card">Check drift and propose trades</Suggestion> <Suggestion suggestion="Draft a post" variant="card">Write an update for the feed</Suggestion> </div></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps } from "react";import { useCallback } from "react";import { Button } from "@/registry/edmi/ui/button";import { ScrollArea, ScrollBar } from "@/registry/edmi/ui/scroll-area";
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
/** Row of suggestions; scrolls horizontally with a fade at both edges on narrow screens. */export const Suggestions = ({ className, children, ...props}: SuggestionsProps) => ( <ScrollArea data-slot="ai-suggestions" className="w-full overflow-x-auto whitespace-nowrap" {...props} > <div className={cn("flex w-max flex-nowrap items-center gap-2", className)}> {children} </div> <ScrollBar className="hidden" orientation="horizontal" /> </ScrollArea>);
type ButtonVariant = NonNullable<ComponentProps<typeof Button>["variant"]>;
export type SuggestionProps = Omit< ComponentProps<typeof Button>, "onClick" | "variant"> & { suggestion: string; onClick?: (suggestion: string) => void; /** * `chip` (default): pill. `card` ✦: a larger tile with a lead-in line, for the home state * (`children` = description). */ variant?: "chip" | "card" | ButtonVariant;};
export const Suggestion = ({ suggestion, onClick, className, variant = "chip", size = "sm", children, ...props}: SuggestionProps) => { const handleClick = useCallback(() => { onClick?.(suggestion); }, [onClick, suggestion]);
if (variant === "card") { return ( <Button data-slot="ai-suggestion" data-variant="card" className={cn( "h-auto min-w-44 flex-col items-start gap-1 rounded-xl px-4 py-3 text-left whitespace-normal", className, )} onClick={handleClick} type="button" variant="outline" {...props} > <span className="text-[13.5px] font-medium">{suggestion}</span> {children && ( <span className="text-xs font-normal text-muted-foreground"> {children} </span> )} </Button> ); }
return ( <Button data-slot="ai-suggestion" data-variant="chip" className={cn("rounded-full px-4", className)} onClick={handleClick} size={size} type="button" variant={variant === "chip" ? "outline" : variant} {...props} > {children || suggestion} </Button> );};<script setup lang="ts">import type { HTMLAttributes } from "vue"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import type { Elevation } from "@/registry/edmi/ui/elevation"
const props = withDefaults(defineProps<{ suggestion: string class?: HTMLAttributes["class"] /** * `chip` (default): pill. `card` ✦: a larger tile with a lead-in line, for the home state * (slot = description). */ variant?: "chip" | "card" | NonNullable<ButtonVariants["variant"]> size?: ButtonVariants["size"] /** ✦ depth: sunken -1, flat 0, raised +1, floating +2 (follows the Button rules). */ elevation?: Elevation}>(), { variant: "chip", size: "sm", elevation: undefined,})
const emit = defineEmits<{ (e: "click", suggestion: string): void}>()
const isCard = computed(() => props.variant === "card")const buttonVariant = computed(() => props.variant === "chip" || props.variant === "card" ? "outline" : props.variant)</script>
<template> <Button data-slot="ai-suggestion" :data-variant="isCard ? 'card' : 'chip'" type="button" :variant="buttonVariant" :size="isCard ? undefined : size" :elevation="elevation" :class="cn( isCard ? 'h-auto min-w-44 flex-col items-start gap-1 rounded-xl px-4 py-3 text-left whitespace-normal' : 'rounded-full px-4', props.class, )" @click="emit('click', suggestion)" > <template v-if="isCard"> <span class="text-[13.5px] font-medium">{{ suggestion }}</span> <span v-if="$slots.default" class="text-xs font-normal text-muted-foreground"><slot /></span> </template> <slot v-else>{{ suggestion }}</slot> </Button></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { ScrollArea, ScrollBar } from "@/registry/edmi/ui/scroll-area"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <!-- Row of suggestions; scrolls horizontally. --> <ScrollArea data-slot="ai-suggestions" class="w-full overflow-x-auto whitespace-nowrap"> <div :class="cn('flex w-max flex-nowrap items-center gap-2', props.class)"> <slot /> </div> <ScrollBar class="hidden" orientation="horizontal" /> </ScrollArea></template>export { default as Suggestion } from "./Suggestion.vue";export { default as Suggestions } from "./Suggestions.vue";import Suggestion from "./suggestion.svelte";import Suggestions from "./suggestions.svelte";
export { Suggestion, Suggestions };<script lang="ts"> import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte";
type ButtonVariant = NonNullable<ButtonProps["variant"]>;
let { suggestion, onclick, class: className, variant = "chip", size = "sm", children, ...restProps }: Omit<ButtonProps, "onclick" | "variant" | "href"> & { suggestion: string; onclick?: (suggestion: string) => void; /** * `chip` (default): pill. `card` ✦: a larger tile with a lead-in line, for the home state * (`children` = description). */ variant?: "chip" | "card" | ButtonVariant; children?: Snippet; } = $props();
const isCard = $derived(variant === "card"); const buttonVariant = $derived<ButtonVariant>( variant === "chip" || variant === "card" ? "outline" : variant );</script>
<Button data-slot="ai-suggestion" data-variant={isCard ? "card" : "chip"} type="button" variant={buttonVariant} size={isCard ? undefined : size} class={cn( isCard ? "h-auto min-w-44 flex-col items-start gap-1 rounded-xl px-4 py-3 text-left whitespace-normal" : "rounded-full px-4", className )} onclick={() => onclick?.(suggestion)} {...restProps}> {#if isCard} <span class="text-[13.5px] font-medium">{suggestion}</span> {#if children} <span class="text-xs font-normal text-muted-foreground">{@render children()}</span> {/if} {:else if children} {@render children()} {:else} {suggestion} {/if}</Button><script lang="ts"> import { ScrollArea } from "$lib/registry/ui/scroll-area/index.js"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte";
let { children, class: className, ...restProps }: { children?: Snippet; class?: string; } = $props();</script>
<!-- Row of suggestions; scrolls horizontally. --><ScrollArea data-slot="ai-suggestions" orientation="horizontal" scrollbarXClasses="hidden" class="w-full whitespace-nowrap" {...restProps}> <div class={cn("flex w-max flex-nowrap items-center gap-2", className)}> {@render children?.()} </div></ScrollArea>Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Suggestions are Buttons, so they follow the Button levels: flat by default, sunken (-1), raised (+1) and floating (+2).
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 { Suggestion } from "@/components/ai/suggestion";
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> <div className="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" elevation={value} /> <Suggestion suggestion="Draft a post" elevation={value} /> <Suggestion suggestion="Explain the keeper" variant="card" elevation={value} > How rebalancing works </Suggestion> </div> </div> ))} </div> );}<script setup lang="ts">import { Suggestion } from "@/components/ai/suggestion";
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> <div class="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" :elevation="level.value" /> <Suggestion suggestion="Draft a post" :elevation="level.value" /> <Suggestion suggestion="Explain the keeper" variant="card" :elevation="level.value">How rebalancing works</Suggestion> </div> </div> </div></template><script lang="ts"> import { Suggestion } from "$lib/components/ai/suggestion";
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> <div class="flex flex-wrap gap-2"> <Suggestion suggestion="Rebalance now" elevation={level.value} /> <Suggestion suggestion="Draft a post" elevation={level.value} /> <Suggestion suggestion="Explain the keeper" variant="card" elevation={level.value}>How rebalancing works</Suggestion> </div> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
| Suggestion | suggestion, onClick |
string, (suggestion) => void |
|
| Suggestion | variant |
chip | card | Button variant |
chip |
| Suggestion | elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”variant="card": a larger tile with a lead-in line;childrenis the muted description.