'use client' import * as React from 'react' import { cn } from '../../utils/cn' import { Tag } from '../ui/tag' import { EntityIcon, type EntityIconValue } from '../icon-display' // ============================================================================= // Types // ============================================================================= /** Accent tint for quick-action chip icons: a named brand token * (`'pink'`/`'cyan'`) or ANY CSS color value coming from admin config * (agent/persona `icon_props.color`). */ export type QuickActionAccent = 'pink' | 'cyan' | (string & {}) /** THE built-in agent→accent pairs ({@link getAgentAccent} derives from it). */ const AGENT_ACCENTS = { fae: 'pink', mingo: 'cyan' } as const /** * FALLBACK-ONLY agent→accent mapping (fae→pink, mingo→cyan) for the built-in * agents when nothing is configured. A color configured on the entity itself — * the quick action's `iconProps.color`, or the agent/persona identity * `icon_props.color` — always wins over this map; callers must resolve the * configured value first and only fall back here. Never re-inline the slug * comparison elsewhere. */ export function getAgentAccent(slug: string | null | undefined): QuickActionAccent | undefined { return slug === 'fae' || slug === 'mingo' ? AGENT_ACCENTS[slug] : undefined } /** Admin-configured identity color (`icon_props.color` on the agent/persona * row) → chip accent. Undefined when the identity doesn't set one. */ export function accentFromIdentityIcon( icon: { props?: Record | null } | null | undefined, ): QuickActionAccent | undefined { const color = icon?.props?.color return typeof color === 'string' && color.length > 0 ? color : undefined } /** * Declarative icon for a quick-action chip, resolved 100% by `` * (url → fae/mingo `AgentMark` → icons-v2 glyph → file fallback): * - "agent format" — `{ name: 'fae' }` / `{ name: 'mingo' }` renders the * packaged agent mark (fills the chip's icon box; `accent` is ignored — the * marks carry their own colors). * - "config format" — `{ name, url, props, accent }` renders the * admin-configured icon; `accent` tints registry glyphs via a * `text-ods-flamingo-*` class on `currentColor`. * * If `props.color` is set it wins over `accent` — the accent is a class tint * on `currentColor`, the prop is an explicit fill (EntityIcon spreads `props` * onto the glyph last). */ export interface QuickActionIconSpec extends EntityIconValue { accent?: QuickActionAccent /** Glyph size in px. Defaults to 16 (the chip design's config-icon size); * agent marks size via the icon box instead. */ size?: number } const ACCENT_CLASS: Record = { pink: 'text-ods-flamingo-pink', cyan: 'text-ods-flamingo-cyan', } /** A spec is a plain data object, never a React element — so any non-element, * non-array object is a spec; strings, elements, fragments, and ReactNode * ARRAYS stay ReactNode. */ export function isQuickActionIconSpec( icon: React.ReactNode | QuickActionIconSpec | undefined, ): icon is QuickActionIconSpec { return typeof icon === 'object' && icon !== null && !Array.isArray(icon) && !React.isValidElement(icon) } /** Resolve a chip `icon` prop to a renderable node. Specs go through * ``; ReactNodes pass through untouched. */ export function renderQuickActionIcon( icon: React.ReactNode | QuickActionIconSpec | undefined, ): React.ReactNode { if (!isQuickActionIconSpec(icon)) { return icon } const spec = icon if (!spec.name && !spec.url) return undefined // Named brand tokens tint via a text class on `currentColor`; any other // accent value is an admin-configured CSS color, delivered as the glyph's // DEFAULT `color` prop — an explicit per-action `props.color` (spread after) // still wins. const accentClass = spec.accent ? ACCENT_CLASS[spec.accent] : undefined const props = spec.accent && !accentClass ? { color: spec.accent, ...(spec.props ?? {}) } : spec.props return ( ) } /** * Compact category/status affix rendered INSIDE the chip at the label's * leading edge (Atlassian-lozenge / M3 leading-slot pattern — category as * text, never color alone). `className` drives the text color (e.g. * `text-ods-warning`); the tinted background derives from `currentColor`, * so one utility styles both. */ export interface QuickActionChipLozenge { label: React.ReactNode className?: string } // ============================================================================= // Themes // ============================================================================= /** * A caller-supplied chip theme — the accent (and optional classification * lozenge) for "who does this work". The lib deliberately ships NO theme * registry and NO fallbacks: agent colors are server-configured (resolve via * {@link accentFromIdentityIcon} on the agent row's `icon_props.color`, or * {@link getAgentAccent} for the built-in agents) and category pairs are * product decisions — the CONSUMER defines its specs and injects them, per * chip or as a wall default. */ export interface QuickActionThemeSpec { /** Chip icon tint ({@link QuickActionAccent}: brand token or CSS color). */ accent: QuickActionAccent /** Classification affix rendered inside the chip (category themes). */ lozenge?: QuickActionChipLozenge } /** Resolve a chip label + optional lozenge to the Tag label node. */ function composeChipLabel( label: React.ReactNode, lozenge: QuickActionChipLozenge | undefined, ): React.ReactNode { if (!lozenge) return label return ( <> {lozenge.label} {label} ) } export interface QuickActionChipButtonProps { label: React.ReactNode /** Icon: a declarative {@link QuickActionIconSpec} (preferred — unified * EntityIcon resolution) or a pre-rendered ReactNode. */ icon?: React.ReactNode | QuickActionIconSpec /** Caller-supplied {@link QuickActionThemeSpec}: supplies the icon accent * when the icon spec doesn't carry its own, and the lozenge when `lozenge` * is `true`. Explicit values always win over the theme. */ theme?: QuickActionThemeSpec /** {@link QuickActionChipLozenge} at the label's leading edge (e.g. an * IT/SEC classification affix). `true` renders the `theme`'s lozenge. */ lozenge?: QuickActionChipLozenge | boolean /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */ variant?: 'primary' | 'outline' /** Active single-select state (Figma "Feature Item" active): renders the * Tag's `selected` variant (pink border + pink-secondary fill), overriding * `variant`. Used by chip groups acting as tabs (OpenFrame categories). */ selected?: boolean /** Accent for the `selected` skin: `'cyan'` uses the cyan twin, anything else * (default) uses pink. Lets an agent's chip group match its own theme accent * (fae pink / mingo cyan) instead of the fixed pink. */ selectedAccent?: QuickActionAccent /** Chip scale, forwarded to `Tag` — `'large'` is the Figma "Feature Item" * 48px chip (h3 bold label, 24px icon box). Default `'default'` (32px). */ size?: 'default' | 'large' onSelect?: () => void /** Pointer/keyboard focus enters the chip — e.g. preview the full prompt. */ onHoverStart?: () => void /** Pointer/keyboard focus leaves the chip — e.g. restore the composer. */ onHoverEnd?: () => void /** `false` renders a plain non-focusable `` (decorative use: marquee * strips, table cells). Default `true`. */ interactive?: boolean className?: string } // ============================================================================= // Skeleton // ============================================================================= export interface QuickActionChipSkeletonProps { /** Label placeholder width in `ch` of the chip's own font — vary per item * for a realistic spread. Default 16. */ labelCh?: number /** Reserve the leading-icon slot (default true — most chips carry one). */ icon?: boolean /** Reserve the leading lozenge affix slot. */ lozenge?: boolean /** Chip scale — MUST match the loaded chips' `size` or the swap jumps. */ size?: 'default' | 'large' className?: string } /** Pulse bar inside the chip skeleton — the standard `ui/skeleton` treatment * (`animate-pulse bg-ods-border`), as a SPAN so it stays valid phrasing * content inside the Tag's label span. */ function ChipSkelBar({ className, style }: { className?: string; style?: React.CSSProperties }) { return } /** * Loading placeholder for {@link QuickActionChipButton} — renders the REAL * `Tag` (same height, padding, border, radius, icon box) with standard * skeleton pulse bars (identical animation + token as `ui/skeleton`) in the * icon/lozenge/label slots, so a skeleton chip is 1:1 with a loaded chip by * construction. Use anywhere quick actions stream in (chat empty states, * marketing walls, deck panels). */ export function QuickActionChipSkeleton({ labelCh = 16, icon = true, lozenge = false, size = 'default', className }: QuickActionChipSkeletonProps) { return ( : undefined} label={ <> {lozenge && } } /> ) } // ============================================================================= // Component // ============================================================================= /** * The unified quick-action chip — Figma chip anatomy (card bg, border, mono * uppercase label) via the `Tag` outline/primary variants, with icon * resolution owned by `` in two formats (config icon w/ agent * accent, or fae/mingo agent mark). Used by every chat empty state * (guide / mingo / ai-agent), the marketing marquee strips, and the ROI * table task cells. */ export function QuickActionChipButton({ label, icon, theme, lozenge, variant = 'outline', selected = false, selectedAccent, size = 'default', onSelect, onHoverStart, onHoverEnd, interactive = true, className, }: QuickActionChipButtonProps) { // The theme's accent only fills the gap — an icon spec's own accent // (admin-configured per-action color) always wins. const themedIcon = theme && isQuickActionIconSpec(icon) && !icon.accent ? { ...icon, accent: theme.accent } : icon const resolvedLozenge = lozenge === true ? theme?.lozenge : lozenge === false ? undefined : lozenge const resolvedIcon = renderQuickActionIcon(themedIcon) const resolvedLabel = composeChipLabel(label, resolvedLozenge) const tagVariant = selected ? (selectedAccent === 'cyan' ? 'selectedCyan' : 'selected') : variant if (!interactive) { return } return ( ) } // ============================================================================= // Chip data shape + data→button mapper // ============================================================================= /** * THE quick-action chip DATA shape — one declarative description of a chip that * every quick-action surface builds (chat empty states, {@link QuickActionWall}, * {@link QuickActionMarquee}). Maps 1:1 to {@link QuickActionChipButton} props * via {@link QuickActionChipFromData}, so adding a chip capability means editing * exactly these two. */ export interface QuickActionChip { /** Stable React key + menu-item id. */ id: string label: string /** Pre-rendered node OR a declarative {@link QuickActionIconSpec} (resolved * via the unified `` path). */ icon?: React.ReactNode | QuickActionIconSpec /** Caller-supplied {@link QuickActionThemeSpec} (accent + optional * lozenge) — per-chip so mixed walls (interleaved IT/SEC streams) work. * The lib ships no theme registry; consumers define their own specs. */ theme?: QuickActionThemeSpec /** Classification affix at the label's leading edge; `true` = the theme's. */ lozenge?: QuickActionChipLozenge | boolean /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */ variant?: 'primary' | 'outline' /** Active single-select state — renders the accented `selected` skin * (overrides `variant`). */ selected?: boolean /** Accent for the `selected` skin (`'cyan'` = cyan twin, else pink). */ selectedAccent?: QuickActionAccent onSelect?: () => void /** Pointer/keyboard focus enters the chip — e.g. preview the full prompt in * the composer. */ onHoverStart?: () => void /** Pointer/keyboard focus leaves the chip — e.g. restore the composer. */ onHoverEnd?: () => void } /** * THE {@link QuickActionChip}-data → {@link QuickActionChipButton} mapper — * one prop-plumbing spelling shared by every chat empty state, `QuickActionWall`, * and `QuickActionMarquee` (adding a chip field means editing exactly here). * `defaultTheme`/`defaultLozenge` fill gaps for wall-level theming; * `interactive={false}` renders the decorative Tag form (loop-clone copies). */ export function QuickActionChipFromData({ chip, defaultTheme, defaultLozenge, interactive = true, className, }: { chip: QuickActionChip defaultTheme?: QuickActionThemeSpec defaultLozenge?: boolean interactive?: boolean className?: string }) { return ( ) }