/** * Role-aware style hooks for chat surfaces. * * Pure utility hooks — no React state, no side effects, just memoized * className strings derived from `bubbleTokens`. They exist so: * - components don't import token constants directly (single facade) * - "the user-bubble link color" can be changed in one file * - tests can mock the hook to assert intent without parsing classNames */ import { useMemo } from 'react'; import { ANCHOR, BUBBLE_SURFACE, DESTRUCTIVE_SURFACE, TOGGLE, TOOL_CALL, type ChatBubbleSurface, } from './bubbleTokens'; export interface ChatBubbleStyles { /** className for the bubble container (background + text + border). */ surface: string; /** className for an inline anchor inside markdown. */ anchor: string; /** className for a secondary inline action (e.g. show more). */ toggle: string; } /** * Resolve bubble + content styles for a single message. * * @param role 'user' | 'assistant' | 'system' (system defaults to assistant styling) * @param isError marks the bubble as a failed turn (overrides surface) * * @example * ```tsx * const { surface, anchor } = useChatBubbleStyles(message.role, !!message.isError); *
* ``` */ export function useChatBubbleStyles( role: 'user' | 'assistant' | 'system', isError: boolean, ): ChatBubbleStyles { return useMemo(() => { const isUser = role === 'user'; const variant: ChatBubbleSurface = isUser ? 'user' : isError ? 'error' : 'assistant'; return { surface: BUBBLE_SURFACE[variant], anchor: isUser ? ANCHOR.user : ANCHOR.assistant, toggle: isUser ? TOGGLE.user : TOGGLE.assistant, }; }, [role, isError]); } export interface ChatRoleStyles { anchor: string; toggle: string; } /** * Lightweight variant when only role matters (no error state, no surface). * Use in shared markdown renderers that don't know about bubble background. */ export function useChatRoleStyles(isUser: boolean): ChatRoleStyles { return useMemo( () => ({ anchor: isUser ? ANCHOR.user : ANCHOR.assistant, toggle: isUser ? TOGGLE.user : TOGGLE.assistant, }), [isUser], ); } export interface ChatDestructiveStyles { banner: string; hover: string; hoverStrong: string; text: string; menuItem: string; toolErrorText: string; } /** * Destructive (delete / error) class facade. Hook form keeps the API * symmetric with the others; under the hood it returns a frozen object. */ export function useChatDestructiveStyles(): ChatDestructiveStyles { return DESTRUCTIVE_STYLES; } const DESTRUCTIVE_STYLES: ChatDestructiveStyles = { banner: DESTRUCTIVE_SURFACE.banner, hover: DESTRUCTIVE_SURFACE.hover, hoverStrong: DESTRUCTIVE_SURFACE.hoverStrong, text: DESTRUCTIVE_SURFACE.text, menuItem: DESTRUCTIVE_SURFACE.menuItem, toolErrorText: TOOL_CALL.errorText, };