registry:block
Recommendation Card
Agent suggestion with a confidence meter, alternatives drawer, and accept action.
Rendered example
Want me to place this restock order?
Reorder waffle cones from cone_king with lead time 7_days.
Other options
Install
orion-thememeter
Open registry JSON registry/ui/recommendation-card.tsx
"use client"
import * as React from "react"
import { Meter } from "@/registry/ui/meter"
/**
* RECOMMENDATION CARD
* The card holds its shape. Pressing "Alternatives" opens a drawer
* listing the other options; picking one promotes it to the
* recommendation. The primary action confirms.
*/
type Option = {
key: string
body: React.ReactNode
short: string
signal: number
tone: string
label: string
cta: string
ctaStyle: string
}
const OPTIONS: Option[] = [
{
key: "high",
body: (
<>
Reorder waffle cones from{" "}
<code className="rounded-md bg-accent-tint px-1.5 py-0.5 font-mono text-[12px] text-accent-ink">cone_king</code>{" "}
with lead time{" "}
<code className="rounded-md bg-accent-tint px-1.5 py-0.5 font-mono text-[12px] text-accent-ink">7_days</code>.
</>
),
short: "Reorder from cone_king · 7-day lead",
signal: 3,
tone: "var(--green)",
label: "High confidence",
cta: "Accept",
ctaStyle: "bg-accent text-white",
},
{
key: "review",
body: (
<>
Switch vanilla to{" "}
<code className="rounded-md bg-orange-tint px-1.5 py-0.5 font-mono text-[12px] text-orange">
vanilla_madagascar
</code>{" "}
for peak season.
</>
),
short: "Switch to vanilla_madagascar",
signal: 2,
tone: "var(--orange)",
label: "Needs review",
cta: "Configure",
ctaStyle: "bg-ink text-canvas",
},
{
key: "none",
body: (
<>
Fall back to a <span className="font-medium text-ink">full restock</span> across every SKU.
</>
),
short: "Full restock across every SKU",
signal: 0,
tone: "var(--ink-3)",
label: "No signal",
cta: "Accept full restock",
ctaStyle: "bg-ink text-canvas",
},
]
export function RecommendationCard() {
const [selected, setSelected] = React.useState(0)
const [open, setOpen] = React.useState(false)
const [accepted, setAccepted] = React.useState(false)
const active = OPTIONS[selected]!
const others = OPTIONS.map((o, i) => ({ o, i })).filter(({ i }) => i !== selected)
return (
<div className="w-full max-w-95 overflow-hidden rounded-card bg-surface shadow-card">
<div className="primitive-card-pad">
<span className="text-[13px] font-medium text-ink">Want me to place this restock order?</span>
<p
key={active.key}
className="mt-1.5 min-h-12 text-[13px] leading-relaxed text-ink-2"
style={{ animation: "fade-in 180ms ease-out both" }}
>
{active.body}
</p>
</div>
<div
className="grid transition-[grid-template-rows,opacity] duration-300"
style={{
gridTemplateRows: open ? "1fr" : "0fr",
opacity: open ? 1 : 0,
transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
}}
>
<div className="overflow-hidden">
<div className="border-t border-line bg-inset px-2 py-2">
<p className="px-1.5 pb-1 text-[11px] font-medium text-ink-3">Other options</p>
{others.map(({ o, i }) => (
<button
key={o.key}
type="button"
onClick={() => {
setSelected(i)
setAccepted(false)
setOpen(false)
}}
className="flex w-full items-center gap-2.5 rounded-control px-1.5 py-1.5 text-left transition-colors duration-100 hover:bg-hover"
>
<Meter signal={o.signal} tone={o.tone} />
<span className="min-w-0 flex-1 truncate text-[12.5px] text-ink">{o.short}</span>
<span className="shrink-0 text-[11px] text-ink-3">{o.label}</span>
</button>
))}
</div>
</div>
</div>
<div className="primitive-card-footer flex items-center justify-between gap-3 border-t border-line bg-inset">
<span className="flex items-center gap-2">
<Meter signal={active.signal} tone={active.tone} />
<span className="text-[12.5px] font-medium text-ink-2">{active.label}</span>
</span>
<span className="-mr-0.5 flex items-center gap-2">
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((current) => !current)}
className={`h-7 rounded-control px-2.5 text-[12.5px] font-medium shadow-btn transition-[background-color,transform] duration-100 active:scale-[0.96] ${
open ? "bg-hover text-ink" : "bg-surface text-ink hover:bg-hover"
}`}
>
Alternatives
</button>
<button
type="button"
onClick={() => setAccepted(true)}
className={`h-7 rounded-control px-3 text-[12.5px] font-medium shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_0_0_1px_rgba(16,24,40,0.12),0_1px_2px_rgba(16,24,40,0.1)] transition-[background-color,transform] duration-150 active:scale-[0.96] ${
accepted ? "bg-green text-white" : active.ctaStyle
}`}
>
{accepted ? "Accepted" : active.cta}
</button>
</span>
</div>
</div>
)
}