"use client" /** * AskLeoButton — chart-card Leo CTA (outline sm, duotone star + label). * * Default `onClick` toggles the Ask Leo sidebar (`useAskLeo`). Pass `onClick` * to override (e.g. inline draft on the new-question composer). * * @see charts-overview.tsx — ChartCard header (canonical usage) * @see AGENTS.md / charts-overview file header — Ask Leo icon guideline */ import * as React from "react" import { AskLeoShortcutKbds, useAskLeo } from "@/components/ask-leo-context" import { Button } from "@/components/ui/button" import { LeoIcon, type LeoIconMotionState, type LeoIconSize, } from "@/components/ui/leo-icon" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" export interface AskLeoButtonProps { /** Matches chart card header (`sm`) or page header actions (`lg`, e.g. Save question). */ size?: "sm" | "lg" /** When true, hides the text label (chart selector variant). */ iconOnly?: boolean /** Button label. Default `"Ask Leo"`. */ label?: string /** Overrides sidebar toggle — use for route-local Leo actions. */ onClick?: () => void disabled?: boolean "aria-busy"?: boolean /** Accessible name; defaults to `label`. */ ariaLabel?: string /** Shown instead of `label` when `aria-busy` is true. */ busyLabel?: string /** Tooltip body before shortcut chips. Defaults to `label`. */ tooltipLabel?: string /** When false, tooltip omits ⌘⌥K hints (non-sidebar actions). Default true. */ showShortcut?: boolean /** * Animated Leo star (`LeoIcon` ambient). Defaults to true when `size="lg"`. * Chart cards keep the static duotone glyph (`size="sm"`). */ animatedStar?: boolean /** * Play one `invited` gesture when the button appears, then settle to `rest`. * For entry points that arrive unannounced (the floating launcher) rather * than ones the user already sees in a toolbar. */ introduceOnMount?: boolean /** * `brand` fills the surface with the brand colour and flips the star to * `--brand-foreground`. Paired here rather than left to callers so the star * can never end up brand-on-brand. */ tone?: "default" | "brand" /** Star box. Defaults to `xs` (20px), which fits the `sm` / `lg` buttons. */ starSize?: LeoIconSize className?: string } export function AskLeoButton({ size = "sm", iconOnly = false, label = "Ask Leo", onClick: onClickProp, disabled = false, "aria-busy": ariaBusy, ariaLabel, busyLabel, tooltipLabel, showShortcut = true, animatedStar, introduceOnMount = false, tone = "default", starSize = "xs", className, }: AskLeoButtonProps) { const { toggle } = useAskLeo() const displayLabel = ariaBusy && busyLabel ? busyLabel : label const tipText = tooltipLabel ?? label const useAnimatedStar = animatedStar ?? size === "lg" // Dropping the label is not enough to make a square trigger: the text sizes // keep their inline padding, which left `lg` iconOnly as a 70x40 pill with a // floating star. Switch to the aspect-square icon sizes instead. const buttonSize = iconOnly ? (size === "lg" ? "icon-lg" : "icon-sm") : size const leoFillClass = tone === "brand" ? // On a brand fill the star has to be the on-brand foreground, not a // brand shade — brand-on-brand would disappear. "[--leo-icon-fill:var(--brand-foreground)]" : "[--leo-icon-fill:var(--brand-color-dark)] dark:[--leo-icon-fill:var(--brand-color-light)]" const toneClass = tone === "brand" ? "border-transparent bg-brand text-brand-foreground hover:bg-brand/90 hover:text-brand-foreground dark:bg-brand dark:hover:bg-brand/90" : undefined // Pointer and keyboard both count as intent, so the star reacts for keyboard // users too rather than rewarding only the mouse. const [invited, setInvited] = React.useState(false) // `invited` is a one-shot gesture, so playing it once means holding the state // for its duration and then settling. Slightly longer than LEO_GESTURE (0.42s) // so the keyframes finish before the state changes under them. const [introducing, setIntroducing] = React.useState(introduceOnMount) React.useEffect(() => { if (!introducing) return const timer = window.setTimeout(() => setIntroducing(false), 520) return () => window.clearTimeout(timer) }, [introducing]) const leoState: LeoIconMotionState = ariaBusy ? "working" : invited || introducing ? "invited" : "rest" const starMark = useAnimatedStar ? ( ) : (