ink
ink copied to clipboard
Titles on Boxes similar to Boxen
@vadimdemedes Just wondering if adding titles to the Box component (similar to how Boxen does it) is on the radar or something that was ever attempted?
Not exactly adding a title prop, but here's something that I wrote some time back to add title to boxes... The reason I decided to have a separate component for the title is so that I can then choose to colorise or add effects to the title...
import React, { useRef, ReactElement, useEffect } from "react"
import { measureElement, Box, Text } from "ink"
type TitleAlignment = 'left' | 'center' | 'right'
export const TitleText = (props: typeof Text.defaultProps) => Text(props)
TitleText.displayName = 'BoxTitle'
TitleText.defaultProps = Text.defaultProps
type TitledBoxProps = {
alignment?: TitleAlignment
} & typeof Box.defaultProps
export const TitledBox = (props: TitledBoxProps) => {
const ref = useRef()
const [calculatedWidth, setCalculatedWidth] = React.useState(0)
const { borderStyle, borderColor, alignment, width, children, ...rest } = props
useEffect(() => {
const { width } = measureElement(ref.current)
setCalculatedWidth(width)
}, [])
const cArray = React.Children.toArray(children)
let textWrapper: JSX.Element
if (cArray.length > 0) {
let cText = cArray.find(child => {
return React.isValidElement(child) && child.type === TitleText
})
if (cText) {
let margin = 0
const textWidth = (cText as ReactElement).props.children.length
switch (alignment) {
case 'left':
break
case 'center':
if (calculatedWidth > 0) {
margin = Math.floor((calculatedWidth - textWidth) / 2)
}
break
case 'right':
if (calculatedWidth > 0) {
margin = calculatedWidth - textWidth - 2 // -2 to cater for the left/right border
}
}
textWrapper = (
<Box marginTop={-1} marginLeft={margin} {...rest}>
{cText}
</Box>
)
}
}
return (
<Box flexDirection="column" alignItems="stretch" {...{ borderStyle, borderColor, width }} ref={ref}>
{textWrapper}
<Box {...rest}>
{React.Children.map(children, child => {
if (React.isValidElement(child) && child.type === TitleText) return // skip title
return child
})}
</Box>
</Box >
)
}
TitledBox.defaultProps = {
alignment: 'left'
}
Some sample code
import React from "react"
import { render, useInput, useApp, Box, Text, Spacer } from "ink"
import { TitledBox, TitleText } from "ink-titledbox"
const App = () => {
const { exit } = useApp()
const [alignmentType, setAlignmentType] = React.useState(0)
useInput((input, key) => {
switch (input) {
case 'q':
exit()
break
case ' ':
setAlignmentType(alignmentType + 1)
break
}
})
const alignments = ['left', 'center', 'right'] as const
return (
<Box borderStyle="classic" width={70} flexDirection="column">
<Box>
<TitledBox borderStyle="double" width={30} alignment={alignments[alignmentType % 3]}>
<TitleText color="yellow"> Hello </TitleText>
<Text wrap="wrap">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</Text>
</TitledBox>
<TitledBox borderStyle="double" width={20} alignment={alignments[(alignmentType + 1) % 3]}>
<TitleText color="blue"> World </TitleText>
<Text wrap="wrap">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</Text>
</TitledBox>
</Box>
<Spacer />
<Text>[Space] to change alignment, [Q] to quit</Text>
</Box>
)
}
render(<App />)
Note that the alignment of the title will break if padding is added (You can of course, check for that prop as well and cater for it in the margin calculation for the text element).
Hope it helps.