/**
* 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,
};