Box(input: BoxProps, children: Node[]): Node
ScrollView(input: ScrollViewProps, children: Node[]): Node
Column(input: Omit<BoxProps, "direction">, children: Node[]): Node
Row(input: Omit<BoxProps, "direction">, children: Node[]): Node
Text(input: TextProps): Node
Input(input: InputProps): Node
Button(input: ButtonProps, children?: Node[]): Node-
TextProps- required:
text(string | StyledText) - optional:
wrap("none" | "word" | "char") - optional:
textOverflow("clip" | "ellipsis") - optional: all
StyleProps - behavior note: explicit newlines always start a new visual row before wrapping
- required:
-
InputProps- optional:
placeholder(string) - optional:
multiline(boolean) - optional:
wrap("none" | "word" | "char") - optional:
onChange,onSubmit,onFocus,onBlur - optional: all
StyleProps - current behavior note:
multilinelets Enter insert\n, butInputis still append-only today - current limitation:
placeholderis accepted by props but is not rendered yet
- optional:
-
ButtonProps- required:
text,onClick - optional:
onKeyDown,onFocus,onBlur - optional: all
StyleProps
- required:
-
BoxProps- optional:
gap(number) - optional:
direction("row" | "column" | "rowReverse" | "columnReverse") - optional: all
StyleProps
- optional:
-
ScrollViewProps- optional: all
Omit<BoxProps, "direction"> - optional:
scrollY(number) - optional:
onScroll((event: ScrollEvent) => void) - behavior note:
ScrollViewalways uses vertical scrolling - behavior note:
scrollYis vertical-only and interpreted in row units
- optional: all
border:{ color: number; style: "square" | "rounded" }borderTop,borderRight,borderBottom,borderLeft:{ color: number }padding: number or string pair ("vertical horizontal")paddingX,paddingYbackground,foreground(number colors)flexGrowwidth,heightminWidth,minHeightmaxWidth,maxHeightmargin: number or string pair ("vertical horizontal")marginX,marginYalignItemsjustifyContentalignSelfflexShrinkflexBasisflexWrapboxSizing:"borderBox" | "contentBox"
gapdirection:"row" | "column" | "rowReverse" | "columnReverse"
ScrollViewis the only public scrolling container in v1- scrolling is vertical-only in v1
scrollYis paint-time state, not tree-shape state, so updates stay on the style-diff path- Rust clamps
scrollY, floors fractional values to whole rows, and decides final visible hit-testing Row,Column, andBoxdo not expose scrolling props in v1
- Public styling/layout surface is the exported
StyleProps+BoxPropsabove. ScrollViewPropsadds the only public scrolling surface in v1:scrollY,onScroll, and scroll methods on the returned node.- Prefer
Row/Columnfor common cases; useBoxwhen you need explicitdirection, including reverse directions. Textwrapping and overflow are renderer-owned behaviors; do not expect JS-side wrapping helpers to be the source of truth.Inputsupports wrapped rendering for its current text, but full editor behavior is still out of scope.
Bring your own palette. letui only accepts numeric RGB values; it does not ship a theme.
const palette = {
fg: 0xf5f7fa,
surface: 0x16181a,
} as const;Rule: pass numeric hex colors (0xRRGGBB), not CSS strings.
If you want the app palette to follow the terminal theme, use the runtime appearance() helper inside ff(...) and switch your numeric palette there.
setStyle(partialStyle)on all nodessetText(nextText)onText,Input,ButtonsetChildren(nextChildren)onBox,ScrollView,Buttonfocus(),blur(),isFocused()on all nodesscrollTo(y),scrollBy(deltaY),scrollToStart(),scrollToEnd(), andscrollNodeIntoView(node)onScrollViewscrollY,viewportHeight,contentHeight, andmaxScrollYsignals onScrollView
For rich text with multiple colors and styles within a single Text node, use StyledText:
import type { StyledText, TextSpan } from "@frixaco/letui";
const styled: StyledText = {
text: "bold red text and normal text",
spans: [
{ start: 0, end: 14, foreground: 0xff0000, bold: true },
{ start: 19, end: 30, foreground: 0xcccccc },
],
};
const node = Text({ text: styled });Each TextSpan supports:
start,end: code point offsets in the source textforeground?: number— text color (hex)background?: number— background color (hex)bold?: booleanitalic?: booleanunderline?: boolean
Spans must be non-overlapping. Line separators are normalized before rendering; a span boundary cannot split a CRLF pair.
- Build containers with
RowandColumn - Keep leaf nodes (
Text,Input,Button) referenced so you can callsetText/setStyle - Update via signals +
ff, not manual redraw loops - If prop meaning is unclear, check exported types in
src/types.ts