/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useCallback, useEffect, useId, useRef, useState } from 'react'; import { Box, Text, ResizeObserver, type DOMElement } from 'ink'; import { theme } from '../../semantic-colors.js'; import { useOverflowActions } from '../../contexts/OverflowContext.js'; /** * Minimum height for the MaxSizedBox component. * This ensures there is room for at least one line of content as well as the * message that content was truncated. */ export const MINIMUM_MAX_HEIGHT = 2; interface MaxSizedBoxProps { children?: React.ReactNode; maxWidth?: number; maxHeight?: number; overflowDirection?: 'top' | 'bottom'; additionalHiddenLinesCount?: number; } /** * A React component that constrains the size of its children and provides * content-aware truncation when the content exceeds the specified `maxHeight`. */ export const MaxSizedBox: React.FC = ({ children, maxWidth, maxHeight, overflowDirection = 'top', additionalHiddenLinesCount = 0, }) => { const id = useId(); const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {}; const observerRef = useRef(null); const [contentHeight, setContentHeight] = useState(0); const onRefChange = useCallback( (node: DOMElement | null) => { if (observerRef.current) { observerRef.current.disconnect(); observerRef.current = null; } if (node && maxHeight !== undefined) { const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) { setContentHeight(entry.contentRect.height); } }); observer.observe(node); observerRef.current = observer; } }, [maxHeight], ); const effectiveMaxHeight = maxHeight !== undefined ? Math.max(Math.round(maxHeight), MINIMUM_MAX_HEIGHT) : undefined; const isOverflowing = (effectiveMaxHeight !== undefined && contentHeight > effectiveMaxHeight) || additionalHiddenLinesCount > 0; // If we're overflowing, we need to hide at least 1 line for the message. const visibleContentHeight = isOverflowing && effectiveMaxHeight !== undefined ? effectiveMaxHeight - 1 : effectiveMaxHeight; const hiddenLinesCount = visibleContentHeight !== undefined ? Math.max(0, contentHeight - visibleContentHeight) : 0; const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount; useEffect(() => { if (totalHiddenLines > 0) { addOverflowingId?.(id); } else { removeOverflowingId?.(id); } return () => { removeOverflowingId?.(id); }; }, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]); if (effectiveMaxHeight === undefined) { return ( {children} ); } const offset = hiddenLinesCount > 0 && overflowDirection === 'top' ? -hiddenLinesCount : 0; return ( {totalHiddenLines > 0 && overflowDirection === 'top' && ( ... first {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '} hidden ... )} {children} {totalHiddenLines > 0 && overflowDirection === 'bottom' && ( ... last {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '} hidden ... )} ); };