| |
| |
| |
| |
| |
|
|
| 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'; |
| import { isNarrowWidth } from '../../utils/isNarrowWidth.js'; |
| import { Command } from '../../key/keyBindings.js'; |
| import { formatCommand } from '../../key/keybindingUtils.js'; |
|
|
| |
| |
| |
| |
| |
| export const MINIMUM_MAX_HEIGHT = 2; |
|
|
| export interface MaxSizedBoxProps { |
| children?: React.ReactNode; |
| maxWidth?: number; |
| maxHeight?: number; |
| overflowDirection?: 'top' | 'bottom'; |
| additionalHiddenLinesCount?: number; |
| paddingX?: number; |
| } |
|
|
| |
| |
| |
| |
| export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({ |
| children, |
| maxWidth, |
| maxHeight, |
| overflowDirection = 'top', |
| additionalHiddenLinesCount = 0, |
| paddingX = 0, |
| }) => { |
| const id = useId(); |
| const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {}; |
| const observerRef = useRef<ResizeObserver | null>(null); |
|
|
| useEffect( |
| () => () => { |
| observerRef.current?.disconnect(); |
| }, |
| [], |
| ); |
|
|
| 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(Math.round(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; |
|
|
| |
| const visibleContentHeight = |
| isOverflowing && effectiveMaxHeight !== undefined |
| ? effectiveMaxHeight - 1 |
| : effectiveMaxHeight; |
|
|
| const hiddenLinesCount = |
| visibleContentHeight !== undefined |
| ? Math.max(0, contentHeight - visibleContentHeight) |
| : 0; |
|
|
| const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount; |
|
|
| const isNarrow = maxWidth !== undefined && isNarrowWidth(maxWidth); |
| const showMoreKey = formatCommand(Command.SHOW_MORE_LINES); |
|
|
| useEffect(() => { |
| if (totalHiddenLines > 0) { |
| addOverflowingId?.(id); |
| } else { |
| removeOverflowingId?.(id); |
| } |
| }, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]); |
|
|
| useEffect( |
| () => () => { |
| removeOverflowingId?.(id); |
| }, |
| [id, removeOverflowingId], |
| ); |
|
|
| if (effectiveMaxHeight === undefined && totalHiddenLines === 0) { |
| return ( |
| <Box flexDirection="column" width={maxWidth}> |
| {children} |
| </Box> |
| ); |
| } |
|
|
| const offset = |
| hiddenLinesCount > 0 && overflowDirection === 'top' ? -hiddenLinesCount : 0; |
|
|
| return ( |
| <Box |
| flexDirection="column" |
| width={maxWidth} |
| maxHeight={effectiveMaxHeight} |
| flexShrink={0} |
| > |
| {totalHiddenLines > 0 && overflowDirection === 'top' && ( |
| <Box paddingX={paddingX}> |
| <Text color={theme.text.secondary} wrap="truncate"> |
| {isNarrow |
| ? `... ${totalHiddenLines} hidden (${showMoreKey}) ...` |
| : `... first ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`} |
| </Text> |
| </Box> |
| )} |
| <Box |
| flexDirection="column" |
| overflow="hidden" |
| flexGrow={0} |
| maxHeight={isOverflowing ? visibleContentHeight : undefined} |
| > |
| <Box |
| flexDirection="column" |
| ref={onRefChange} |
| flexShrink={0} |
| marginTop={offset} |
| > |
| {children} |
| </Box> |
| </Box> |
| {totalHiddenLines > 0 && overflowDirection === 'bottom' && ( |
| <Box paddingX={paddingX}> |
| <Text color={theme.text.secondary} wrap="truncate"> |
| {isNarrow |
| ? `... ${totalHiddenLines} hidden (${showMoreKey}) ...` |
| : `... last ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`} |
| </Text> |
| </Box> |
| )} |
| </Box> |
| ); |
| }; |
|
|