mirror of
https://github.com/codeaashu/claude-code.git
synced 2026-04-08 22:28:48 +03:00
329 lines
10 KiB
TypeScript
329 lines
10 KiB
TypeScript
import type { CSSProperties } from 'react'
|
|
import { inkColorToCSS } from './color-mapping'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dimension helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Convert an Ink dimension (number = character cells, or `"50%"` percent string)
|
|
* to a CSS value. We use `ch` for character-width units so the layout
|
|
* approximates the terminal in a monospace font.
|
|
*/
|
|
function toCSSSize(value: number | string | undefined): string | undefined {
|
|
if (value === undefined) return undefined
|
|
if (typeof value === 'string') return value // already "50%" etc.
|
|
return `${value}ch`
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Border style mapping
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Maps Ink/cli-boxes border style names to CSS border-style values.
|
|
const BORDER_STYLE_MAP: Record<string, CSSProperties['borderStyle']> = {
|
|
single: 'solid',
|
|
double: 'double',
|
|
round: 'solid', // approximated; borderRadius added below
|
|
bold: 'solid',
|
|
singleDouble: 'solid',
|
|
doubleSingle: 'solid',
|
|
classic: 'solid',
|
|
arrow: 'solid',
|
|
ascii: 'solid',
|
|
dashed: 'dashed',
|
|
// cli-boxes names
|
|
none: 'none',
|
|
}
|
|
|
|
const BORDER_BOLD_STYLES = new Set(['bold'])
|
|
const BORDER_ROUND_STYLES = new Set(['round'])
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main mapping function
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type InkStyleProps = {
|
|
// Position
|
|
position?: 'absolute' | 'relative'
|
|
top?: number | string
|
|
bottom?: number | string
|
|
left?: number | string
|
|
right?: number | string
|
|
|
|
// Margin
|
|
margin?: number
|
|
marginX?: number
|
|
marginY?: number
|
|
marginTop?: number
|
|
marginBottom?: number
|
|
marginLeft?: number
|
|
marginRight?: number
|
|
|
|
// Padding
|
|
padding?: number
|
|
paddingX?: number
|
|
paddingY?: number
|
|
paddingTop?: number
|
|
paddingBottom?: number
|
|
paddingLeft?: number
|
|
paddingRight?: number
|
|
|
|
// Flex
|
|
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse'
|
|
flexGrow?: number
|
|
flexShrink?: number
|
|
flexBasis?: number | string
|
|
flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse'
|
|
alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch'
|
|
alignSelf?: 'flex-start' | 'center' | 'flex-end' | 'auto'
|
|
justifyContent?: 'flex-start' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly' | 'center'
|
|
|
|
// Gap
|
|
gap?: number
|
|
columnGap?: number
|
|
rowGap?: number
|
|
|
|
// Sizing
|
|
width?: number | string
|
|
height?: number | string
|
|
minWidth?: number | string
|
|
minHeight?: number | string
|
|
maxWidth?: number | string
|
|
maxHeight?: number | string
|
|
|
|
// Display
|
|
display?: 'flex' | 'none'
|
|
|
|
// Overflow (Ink only has 'hidden')
|
|
overflow?: 'hidden' | 'visible'
|
|
overflowX?: 'hidden' | 'visible'
|
|
overflowY?: 'hidden' | 'visible'
|
|
|
|
// Border
|
|
borderStyle?: string | { top: string; bottom: string; left: string; right: string; topLeft: string; topRight: string; bottomLeft: string; bottomRight: string }
|
|
borderTop?: boolean
|
|
borderBottom?: boolean
|
|
borderLeft?: boolean
|
|
borderRight?: boolean
|
|
borderColor?: string
|
|
borderTopColor?: string
|
|
borderBottomColor?: string
|
|
borderLeftColor?: string
|
|
borderRightColor?: string
|
|
borderDimColor?: boolean
|
|
borderTopDimColor?: boolean
|
|
borderBottomDimColor?: boolean
|
|
borderLeftDimColor?: boolean
|
|
borderRightDimColor?: boolean
|
|
}
|
|
|
|
export type InkTextStyleProps = {
|
|
color?: string
|
|
backgroundColor?: string
|
|
bold?: boolean
|
|
dim?: boolean
|
|
italic?: boolean
|
|
underline?: boolean
|
|
strikethrough?: boolean
|
|
inverse?: boolean
|
|
wrap?: string
|
|
}
|
|
|
|
/**
|
|
* Convert Ink Box layout props to a React CSSProperties object.
|
|
*/
|
|
export function inkBoxPropsToCSS(props: InkStyleProps): CSSProperties {
|
|
const css: CSSProperties = {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
boxSizing: 'border-box',
|
|
}
|
|
|
|
// Display
|
|
if (props.display === 'none') {
|
|
css.display = 'none'
|
|
return css
|
|
}
|
|
|
|
// Position
|
|
if (props.position) css.position = props.position
|
|
if (props.top !== undefined) css.top = toCSSSize(props.top)
|
|
if (props.bottom !== undefined) css.bottom = toCSSSize(props.bottom)
|
|
if (props.left !== undefined) css.left = toCSSSize(props.left)
|
|
if (props.right !== undefined) css.right = toCSSSize(props.right)
|
|
|
|
// Flex
|
|
if (props.flexDirection) css.flexDirection = props.flexDirection
|
|
if (props.flexGrow !== undefined) css.flexGrow = props.flexGrow
|
|
if (props.flexShrink !== undefined) css.flexShrink = props.flexShrink
|
|
if (props.flexBasis !== undefined) css.flexBasis = toCSSSize(props.flexBasis)
|
|
if (props.flexWrap) css.flexWrap = props.flexWrap
|
|
if (props.alignItems) css.alignItems = props.alignItems
|
|
if (props.alignSelf) css.alignSelf = props.alignSelf
|
|
if (props.justifyContent) css.justifyContent = props.justifyContent
|
|
|
|
// Gap
|
|
if (props.gap !== undefined) css.gap = `${props.gap}ch`
|
|
if (props.columnGap !== undefined) css.columnGap = `${props.columnGap}ch`
|
|
if (props.rowGap !== undefined) css.rowGap = `${props.rowGap}ch`
|
|
|
|
// Sizing
|
|
if (props.width !== undefined) css.width = toCSSSize(props.width)
|
|
if (props.height !== undefined) css.height = toCSSSize(props.height)
|
|
if (props.minWidth !== undefined) css.minWidth = toCSSSize(props.minWidth)
|
|
if (props.minHeight !== undefined) css.minHeight = toCSSSize(props.minHeight)
|
|
if (props.maxWidth !== undefined) css.maxWidth = toCSSSize(props.maxWidth)
|
|
if (props.maxHeight !== undefined) css.maxHeight = toCSSSize(props.maxHeight)
|
|
|
|
// Margin (shorthand resolution: margin → marginX/Y → individual sides)
|
|
const mt = props.marginTop ?? props.marginY ?? props.margin
|
|
const mb = props.marginBottom ?? props.marginY ?? props.margin
|
|
const ml = props.marginLeft ?? props.marginX ?? props.margin
|
|
const mr = props.marginRight ?? props.marginX ?? props.margin
|
|
if (mt !== undefined) css.marginTop = toCSSSize(mt)
|
|
if (mb !== undefined) css.marginBottom = toCSSSize(mb)
|
|
if (ml !== undefined) css.marginLeft = toCSSSize(ml)
|
|
if (mr !== undefined) css.marginRight = toCSSSize(mr)
|
|
|
|
// Padding
|
|
const pt = props.paddingTop ?? props.paddingY ?? props.padding
|
|
const pb = props.paddingBottom ?? props.paddingY ?? props.padding
|
|
const pl = props.paddingLeft ?? props.paddingX ?? props.padding
|
|
const pr = props.paddingRight ?? props.paddingX ?? props.padding
|
|
if (pt !== undefined) css.paddingTop = toCSSSize(pt)
|
|
if (pb !== undefined) css.paddingBottom = toCSSSize(pb)
|
|
if (pl !== undefined) css.paddingLeft = toCSSSize(pl)
|
|
if (pr !== undefined) css.paddingRight = toCSSSize(pr)
|
|
|
|
// Overflow
|
|
if (props.overflow) css.overflow = props.overflow
|
|
if (props.overflowX) css.overflowX = props.overflowX
|
|
if (props.overflowY) css.overflowY = props.overflowY
|
|
|
|
// Border
|
|
if (props.borderStyle) {
|
|
const styleName = typeof props.borderStyle === 'string' ? props.borderStyle : 'single'
|
|
const cssBorderStyle = BORDER_STYLE_MAP[styleName] ?? 'solid'
|
|
const isBold = BORDER_BOLD_STYLES.has(styleName)
|
|
const borderWidth = isBold ? '2px' : '1px'
|
|
|
|
const showTop = props.borderTop !== false
|
|
const showBottom = props.borderBottom !== false
|
|
const showLeft = props.borderLeft !== false
|
|
const showRight = props.borderRight !== false
|
|
|
|
const resolveColor = (side: string | undefined, fallback: string | undefined, dim: boolean | undefined) => {
|
|
const raw = side ?? fallback
|
|
const cssColor = inkColorToCSS(raw) ?? 'currentColor'
|
|
return dim ? `color-mix(in srgb, ${cssColor} 60%, transparent)` : cssColor
|
|
}
|
|
|
|
const dimAll = props.borderDimColor
|
|
|
|
if (showTop) {
|
|
css.borderTopStyle = cssBorderStyle
|
|
css.borderTopWidth = borderWidth
|
|
css.borderTopColor = resolveColor(
|
|
props.borderTopColor ?? props.borderColor,
|
|
props.borderColor,
|
|
props.borderTopDimColor ?? dimAll,
|
|
)
|
|
}
|
|
if (showBottom) {
|
|
css.borderBottomStyle = cssBorderStyle
|
|
css.borderBottomWidth = borderWidth
|
|
css.borderBottomColor = resolveColor(
|
|
props.borderBottomColor ?? props.borderColor,
|
|
props.borderColor,
|
|
props.borderBottomDimColor ?? dimAll,
|
|
)
|
|
}
|
|
if (showLeft) {
|
|
css.borderLeftStyle = cssBorderStyle
|
|
css.borderLeftWidth = borderWidth
|
|
css.borderLeftColor = resolveColor(
|
|
props.borderLeftColor ?? props.borderColor,
|
|
props.borderColor,
|
|
props.borderLeftDimColor ?? dimAll,
|
|
)
|
|
}
|
|
if (showRight) {
|
|
css.borderRightStyle = cssBorderStyle
|
|
css.borderRightWidth = borderWidth
|
|
css.borderRightColor = resolveColor(
|
|
props.borderRightColor ?? props.borderColor,
|
|
props.borderColor,
|
|
props.borderRightDimColor ?? dimAll,
|
|
)
|
|
}
|
|
|
|
if (BORDER_ROUND_STYLES.has(styleName)) {
|
|
css.borderRadius = '4px'
|
|
}
|
|
}
|
|
|
|
return css
|
|
}
|
|
|
|
/**
|
|
* Convert Ink Text style props to a React CSSProperties object.
|
|
*/
|
|
export function inkTextPropsToCSS(props: InkTextStyleProps): CSSProperties {
|
|
const css: CSSProperties = {}
|
|
|
|
const fg = inkColorToCSS(props.color)
|
|
const bg = inkColorToCSS(props.backgroundColor)
|
|
|
|
if (props.inverse) {
|
|
// Swap foreground and background
|
|
if (fg) css.backgroundColor = fg
|
|
if (bg) css.color = bg
|
|
if (!fg) css.filter = 'invert(1)'
|
|
} else {
|
|
if (fg) css.color = fg
|
|
if (bg) css.backgroundColor = bg
|
|
}
|
|
|
|
if (props.bold) css.fontWeight = 'bold'
|
|
if (props.dim) css.opacity = 0.6
|
|
if (props.italic) css.fontStyle = 'italic'
|
|
|
|
const decorations: string[] = []
|
|
if (props.underline) decorations.push('underline')
|
|
if (props.strikethrough) decorations.push('line-through')
|
|
if (decorations.length > 0) css.textDecoration = decorations.join(' ')
|
|
|
|
if (props.wrap) {
|
|
switch (props.wrap) {
|
|
case 'wrap':
|
|
case 'wrap-trim':
|
|
css.whiteSpace = 'pre-wrap'
|
|
css.overflowWrap = 'anywhere'
|
|
break
|
|
case 'truncate':
|
|
case 'truncate-end':
|
|
case 'end':
|
|
css.overflow = 'hidden'
|
|
css.textOverflow = 'ellipsis'
|
|
css.whiteSpace = 'nowrap'
|
|
break
|
|
case 'truncate-middle':
|
|
case 'middle':
|
|
// CSS can't do mid-truncation; use ellipsis as fallback
|
|
css.overflow = 'hidden'
|
|
css.textOverflow = 'ellipsis'
|
|
css.whiteSpace = 'nowrap'
|
|
break
|
|
case 'truncate-start':
|
|
css.overflow = 'hidden'
|
|
css.direction = 'rtl'
|
|
css.textOverflow = 'ellipsis'
|
|
css.whiteSpace = 'nowrap'
|
|
break
|
|
}
|
|
}
|
|
|
|
return css
|
|
}
|