"use client" /** * The Ask Leo launcher — the chip that opens Leo from the shell's utility bar. * * Every element the launcher's stylesheet targets is created here: * `data-ask-leo-utility-glow-wrap` and its `data-intro`, the glow layer, the * wash, the star wrapper, and the label. That is the whole reason this component * exists rather than the five consumer files it replaces. The arrival is a CSS * animation shipped in the package's `globals.css`, and a stylesheet that * selects markup it does not own is a release that lands inert: consumers took * the new CSS, kept their own older chip, and saw nothing change. * * Leo's own state stays with the consumer. `open`, `busy`, and `onToggle` are * props because which thread is open — and whether it is mid-answer — is the * app's model, and the package cannot ship the provider that holds it. The * arrival, the greeting, and the chrome are the package's, so they are not * props. * * `showLabel` is the density decision, not a style: at `false` the chip is * icon-only and the arrival's opening beat has nothing to open, which is what * the compact bar wants. The label's box animating from zero is what takes the * comfortable chip from icon to icon-and-name on load. */ import * as React from "react" import { Button } from "../ui/button" import { LeoIcon, type LeoIconSize } from "../ui/leo-icon" import { Tooltip, TooltipContent, TooltipTrigger, } from "../ui/tooltip" import { useOneShotIntro } from "../../hooks/use-one-shot-intro" import { useStagedGreeting } from "../../hooks/use-staged-greeting" import { cn } from "../../lib/utils" import { AskLeoLauncherWash } from "./ask-leo-launcher-wash" import { askLeoLauncherChipClass, utilityBarActionButtonClass, } from "./utility-bar-chrome" export function AskLeoLauncher({ open, busy = false, onToggle, showLabel = false, label = "Ask Leo", shortcut, introId = "ask-leo-launcher", arrival = true, disabled = false, ariaLabel, tooltipLabel, size = "sm", animatedStar = true, starSize = "xs", iconClassName, buttonClassName, className, }: { /** True while Leo's panel is open. Drives `data-leo-open` and the tooltip. */ open: boolean /** True while Leo is answering. Lifts the wash, nothing else. */ busy?: boolean onToggle: () => void /** When true, show the label beside the icon (the comfortable utility bar). */ showLabel?: boolean label?: string /** * The chord, rendered in the tooltip beside the name. * * A node rather than a string because the keys are the consumer's `Kbd` * composition and their chord is theirs to choose. */ shortcut?: React.ReactNode /** * Which arrival this chip counts as. * * Per id and per page load, so a launcher that unmounts and remounts while * navigating does not replay its arrival. Only worth setting if a surface * mounts a second launcher that should arrive on its own. * * `null` opts out of the record and replays on every mount. That is for the * documentation previews: in the shell a replay is noise, but on a page whose * subject is the animation, an arrival that finished before you scrolled to it * documents nothing. */ introId?: string | null /** Disable the one-shot shell arrival for route-local action buttons. */ arrival?: boolean disabled?: boolean ariaLabel?: string tooltipLabel?: string size?: "sm" | "lg" animatedStar?: boolean starSize?: LeoIconSize iconClassName?: string buttonClassName?: string className?: string }) { /* Owned here rather than by the bar, which outlives this chip: the bar renders on routes where the launcher is absent, and an arrival started up there spends its ceiling with nothing on screen — so the chip arrives having already used it up. The id keeps that from becoming the opposite bug, a replay on every hop back. */ const glowIntro = useOneShotIntro(introId ?? undefined, arrival) const greeting = useStagedGreeting(glowIntro.active) const [hovered, setHovered] = React.useState(false) const accessibleName = ariaLabel ?? (open ? `Close ${label}` : label) const tipText = tooltipLabel ?? accessibleName return (
{ if (event.animationName === "ask-leo-utility-glow-intro") { glowIntro.end() } }} />