'use client' import { type ReactNode } from 'react' import { useIsTruncated } from '../../hooks/ui/use-is-truncated' import { cn } from '../../utils/cn' import { FloatingTooltip } from './floating-tooltip' /** ODS typography variants. Maps to the `.text-h1`…`.text-h6` utilities. */ export type TruncateTextVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' /** ODS text tone. Maps to the `text-ods-text-*` colour utilities. */ export type TruncateTextTone = 'primary' | 'secondary' export interface TruncateTextProps { children: string /** Tooltip content; defaults to `children`. */ tooltip?: ReactNode /** Extra classes merged after the variant/tone defaults. */ className?: string side?: 'top' | 'right' | 'bottom' | 'left' /** Max visible lines. `1` uses `truncate` (single-line ellipsis); higher values use `line-clamp-N`. */ lines?: 1 | 2 | 3 | 4 | 5 | 6 /** ODS typography token. Default: `'h4'` (body). */ variant?: TruncateTextVariant /** ODS text tone. Default: `'primary'`. */ tone?: TruncateTextTone /** Force the monospace (heading) font family — preserves the variant's size while swapping family. */ mono?: boolean /** * Extra classes for the tooltip trigger wrapper — the element that becomes the * flex/grid item in the caller's layout (e.g. `flex-1`). Merged after the * built-in `min-w-0 max-w-full`, which is what lets the trigger shrink inside * flex rows so the text ellipsizes instead of clipping. */ triggerClassName?: string /** * Trigger wrapper element. Default `'div'`. Use `'span'` where a block element * is invalid HTML — inside a `

`, a heading, or another ``; the span * trigger is `inline-block` so the inner truncation still measures. */ as?: 'div' | 'span' } const VARIANT_CLASS: Record = { h1: 'text-h1', h2: 'text-h2', h3: 'text-h3', h4: 'text-h4', h5: 'text-h5', h6: 'text-h6', } const TONE_CLASS: Record = { primary: 'text-ods-text-primary', secondary: 'text-ods-text-secondary', } const LINE_CLAMP_CLASS: Record<2 | 3 | 4 | 5 | 6, string> = { 2: 'line-clamp-2', 3: 'line-clamp-3', 4: 'line-clamp-4', 5: 'line-clamp-5', 6: 'line-clamp-6', } /** * Truncated text bound to the ODS typography system. Shows a `FloatingTooltip` * with the full value when (and only when) the content overflows. * * ```tsx * {name} // h4 / primary * {email} // caption * {description} // 3-line clamp * ``` */ export function TruncateText({ children, tooltip, className, side = 'top', lines = 1, variant = 'h4', tone = 'primary', mono = false, triggerClassName, as = 'div', }: TruncateTextProps) { const isMultiLine = lines > 1 const { ref, isTruncated } = useIsTruncated(children, { multiline: isMultiLine }) const clampClass = isMultiLine ? LINE_CLAMP_CLASS[lines as Exclude] : 'truncate block' return ( {children} ) }