'use client'; import { Bot } from 'lucide-react'; import type { CSSProperties, ReactNode } from 'react'; import { useIsPhone, useIsTabletOrBelow } from '@djangocfg/ui-core/hooks'; import { cn } from '@djangocfg/ui-core/lib'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@djangocfg/ui-core/components'; import { Z_INDEX } from '../constants'; export type ChatFABPosition = | 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; export type ChatFABVariant = 'simple' | 'animated' | 'glass'; export type ChatFABSize = 'sm' | 'md' | 'lg' | 'responsive'; export interface ChatFABProps { /** Click handler — typically toggles a `ChatDock`. */ onClick: () => void; /** Accessible label. */ ariaLabel?: string; /** Icon inside the FAB. Defaults to a bot glyph. */ icon?: ReactNode; /** Visual style. @default 'simple' */ variant?: ChatFABVariant; /** Button size. @default 'md' */ size?: ChatFABSize; /** Fixed-screen position. @default 'bottom-right' */ position?: ChatFABPosition; /** Pixel offset from screen edges. @default 24 */ offset?: number; /** z-index for the button. @default 99 — just below the open dock. */ zIndex?: number; /** Show a small attention dot (unread / new). */ pulse?: boolean; /** * Numeric badge — unread count. Numbers > 9 render as "9+". * Overrides `pulse` when both set. */ badge?: number; /** Hover tooltip text. Shows next to the FAB on hover/focus. */ tooltip?: string; /** * Render in-place (no fixed positioning) so the FAB sits inline in the * normal document flow. Useful for stories, screenshots, and previews * inside a contained playground panel. @default false */ inline?: boolean; /** Override classes on the button itself. */ className?: string; /** Extra style on the button (caller-controlled overrides). */ style?: CSSProperties; } type ChatFABFixedSize = Exclude; const SIZE_PX: Record = { sm: 44, md: 56, lg: 64 }; const ICON_PX: Record = { sm: 18, md: 22, lg: 26 }; /** * Resolve `size='responsive'` to a concrete fixed size based on the * viewport. Phone → `sm`, tablet → `md`, desktop → `lg`. `inline` * previews always collapse to `md` so stories stay stable. */ function useEffectiveFABSize(size: ChatFABSize, inline: boolean): ChatFABFixedSize { const isPhone = useIsPhone(); const isBelowDesktop = useIsTabletOrBelow(); if (size !== 'responsive') return size; if (inline) return 'md'; if (isPhone) return 'sm'; if (isBelowDesktop) return 'md'; return 'lg'; } function positionStyle(position: ChatFABPosition, offset: number): CSSProperties { const [vert, horiz] = position.split('-') as ['bottom' | 'top', 'right' | 'left']; return { [vert]: offset, [horiz]: offset } as CSSProperties; } function tooltipSide(position: ChatFABPosition): 'left' | 'right' { // Tooltip sits opposite-horizontal to the FAB so it doesn't run off-screen. return position.endsWith('right') ? 'left' : 'right'; } function Badge({ value }: { value: number }) { const display = value > 9 ? '9+' : String(value); return ( ); } function PulseDot() { return (