Files
inkreach-agent/docs/ui-ref/beautifului/recommendation-card.json

21 lines
6.9 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "recommendation-card",
"type": "registry:component",
"title": "Recommendation Card",
"description": "Agent suggestion with a confidence meter and actions.",
"registryDependencies": [
"https://www.beautifului.dev/r/foundation.json",
"https://www.beautifului.dev/r/button.json",
"https://www.beautifului.dev/r/entity-chip.json",
"https://www.beautifului.dev/r/value-pill.json"
],
"files": [
{
"path": "components/primitives/RecommendationCard.tsx",
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Button, type ButtonVariant } from \"@/components/atoms/Button\";\nimport { EntityChip } from \"@/components/atoms/EntityChip\";\nimport { ValuePill } from \"@/components/atoms/ValuePill\";\n\n/* ─────────────────────────────────────────────────────────\n * RECOMMENDATION CARD\n * The card holds its shape. Pressing \"Alternatives\" opens a\n * new drawer listing the other options; picking one promotes\n * it to the recommendation. The primary action confirms.\n * ───────────────────────────────────────────────────────── */\n\nexport type RecommendationOption = {\n key: string;\n body: React.ReactNode;\n short: string;\n signal: number;\n tone: string;\n label: string;\n cta: string;\n ctaVariant: ButtonVariant;\n};\n\nexport type RecommendationLabels = {\n title: string;\n alternatives: string;\n otherOptions: string;\n accepted: string;\n};\n\nconst DEFAULT_LABELS: RecommendationLabels = {\n title: \"Want me to place this restock order?\",\n alternatives: \"Alternatives\",\n otherOptions: \"Other options\",\n accepted: \"Accepted\",\n};\n\nconst OPTIONS: RecommendationOption[] = [\n {\n key: \"high\",\n body: (\n <>\n Reorder waffle cones from{\" \"}\n <EntityChip name=\"Cone King\" />{\" \"}\n with lead time <ValuePill tone=\"green\">7 days</ValuePill>\n </>\n ),\n short: \"Reorder from Cone King · 7-day lead\",\n signal: 3,\n tone: \"var(--green)\",\n label: \"High confidence\",\n cta: \"Accept\",\n ctaVariant: \"accent\",\n },\n {\n key: \"review\",\n body: (\n <>\n Switch vanilla to <ValuePill>Vanilla Madagascar</ValuePill> for peak season.\n </>\n ),\n short: \"Switch to Vanilla Madagascar\",\n signal: 2,\n tone: \"var(--orange)\",\n label: \"Needs review\",\n cta: \"Configure\",\n ctaVariant: \"primary\",\n },\n {\n key: \"none\",\n body: (\n <>\n Fall back to a <span className=\"font-medium text-ink\">full restock</span> across every SKU.\n </>\n ),\n short: \"Full restock across every SKU\",\n signal: 0,\n tone: \"var(--ink-3)\",\n label: \"No signal\",\n cta: \"Accept full restock\",\n ctaVariant: \"primary\",\n },\n];\n\nfunction Meter({ signal, tone }: { signal: number; tone: string }) {\n return (\n <span className=\"flex items-end gap-0.5\">\n {[0, 1, 2].map((bar) => (\n <span\n key={bar}\n className=\"w-1 rounded-full transition-colors duration-300\"\n style={{ height: 10, background: bar < signal ? tone : \"var(--line-strong)\" }}\n />\n ))}\n </span>\n );\n}\n\nexport default function RecommendationCard({\n options = OPTIONS,\n labels,\n}: {\n options?: RecommendationOption[];\n labels?: Partial<RecommendationLabels>;\n variant?: string;\n} = {}) {\n const t = { ...DEFAULT_LABELS, ...labels };\n const [selected, setSelected] = useState(0);\n const [open, setOpen] = useState(false);\n const [accepted, setAccepted] = useState(false);\n\n const active = options[selected];\n const others = options.map((o, i) => ({ o, i })).filter(({ i }) => i !== selected);\n\n return (\n <div className=\"w-full max-w-95 overflow-hidden rounded-card bg-surface shadow-card\">\n <div className=\"primitive-card-pad\">\n <span className=\"text-[14px] font-medium text-ink\">\n {t.title}\n </span>\n <p\n key={active.key}\n className=\"mt-1.5 min-h-12 text-[13px] leading-relaxed text-ink-2\"\n style={{ animation: \"fade-in 180ms ease-out both\" }}\n >\n {active.body}\n </p>\n </div>\n\n {/* alternatives drawer — a distinctly new section of the card */}\n <div\n className=\"grid transition-[grid-template-rows,opacity] duration-300\"\n style={{\n gridTemplateRows: open ? \"1fr\" : \"0fr\",\n opacity: open ? 1 : 0,\n transitionTimingFunction: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n }}\n >\n <div className=\"overflow-hidden\">\n <div className=\"border-t border-line bg-surface px-2 py-2\">\n <p className=\"px-1.5 pb-1 text-[11px] font-medium text-ink-3\">\n {t.otherOptions}\n </p>\n {others.map(({ o, i }) => (\n <button\n key={o.key}\n type=\"button\"\n onClick={() => {\n setSelected(i);\n setAccepted(false);\n }}\n className=\"flex w-full items-center gap-2.5 rounded-control px-1.5 py-1.5\n text-left transition-colors duration-100 hover:bg-hover\"\n >\n <Meter signal={o.signal} tone={o.tone} />\n <span className=\"min-w-0 flex-1 truncate text-[12.5px] text-ink\">{o.short}</span>\n <span className=\"shrink-0 text-[11px] text-ink-3\">{o.label}</span>\n </button>\n ))}\n </div>\n </div>\n </div>\n\n <div className=\"primitive-card-footer flex items-center justify-between gap-3 bg-surface\">\n <span className=\"flex items-center gap-2\">\n <Meter signal={active.signal} tone={active.tone} />\n <span className=\"text-[12.5px] font-medium text-ink-2\">{active.label}</span>\n </span>\n\n <span className=\"-mr-0.5 flex items-center gap-2\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n aria-expanded={open}\n onClick={() => setOpen((current) => !current)}\n className=\"px-2.5 text-[12.5px]\"\n >\n {t.alternatives}\n </Button>\n <Button\n variant={accepted ? \"success\" : active.ctaVariant}\n size=\"sm\"\n onClick={() => setAccepted(true)}\n className=\"text-[12.5px]\"\n >\n {accepted ? t.accepted : active.cta}\n </Button>\n </span>\n </div>\n </div>\n );\n}\n",
"type": "registry:component",
"target": "components/primitives/RecommendationCard.tsx"
}
]
}