Skip to content

Commit 8830c98

Browse files
committed
feat(tui): add advanced iocraft components and styling features
Add comprehensive terminal UI enhancements including new components, extended border styles, and ANSI 256-color support. **New Components:** - MixedText: Render text with multiple styled sections for syntax highlighting - Fragment: Group elements without layout impact for cleaner composition **Extended Border Styles:** - double-left-right: Double-line borders on sides, single on top/bottom - double-top-bottom: Double-line borders on top/bottom, single on sides - classic: ASCII-only borders using +|- **Custom Border Characters:** - Full control over border rendering with custom character sets - Support for any Unicode characters in border decoration **ANSI 256-Color Support:** - Use extended color palette with 'ansi:123' or bare '196' notation - Applies to text color, border color, and background color - Enables vibrant terminal output beyond basic 16 colors **Comprehensive Tests:** - 26 new tests covering all components and features - Tests for MixedText, Fragment, border styles, custom chars, ANSI colors - Complex scenario testing with nested components Note: MixedText rendering requires updated iocraft .node binary from socket-btm.
1 parent 7db21ad commit 8830c98

File tree

3 files changed

+596
-4
lines changed

3 files changed

+596
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
### Added
1010

11+
- Advanced TUI components and styling for rich terminal interfaces:
12+
- **MixedText component**: Render text with multiple styled sections, perfect for syntax highlighting and rich formatting
13+
- **Fragment component**: Group elements without layout impact, enabling cleaner component composition
14+
- **Extended border styles**: double-left-right, double-top-bottom, and classic ASCII borders
15+
- **Custom border characters**: Full control over border rendering with custom character sets
16+
- **ANSI 256-color support**: Use extended color palette with `ansi:123` or bare number notation for vibrant terminal output
1117
- Comprehensive TUI styling and layout properties for terminal interfaces:
1218
- Text styling: weight (normal, bold, light), dimColor for faded appearance, strikethrough decoration
1319
- Text layout: align (left, center, right), wrap (wrap, nowrap) for content control

packages/cli/src/utils/terminal/iocraft.mts

Lines changed: 224 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,24 @@ export type TextWrap = 'wrap' | 'nowrap'
7272

7373
/**
7474
* Text styling options for visual appearance.
75+
*
76+
* @example
77+
* ```typescript
78+
* // Named colors
79+
* Text({ children: 'Red text', color: 'red' })
80+
*
81+
* // Hex colors
82+
* Text({ children: 'Custom', color: '#FF5733' })
83+
*
84+
* // ANSI 256 colors
85+
* Text({ children: 'Orange', color: 'ansi:208' })
86+
* Text({ children: 'Pink', color: '213' }) // Bare number also works
87+
* ```
7588
*/
7689
export interface TextStyle {
7790
/** Apply bold styling to text */
7891
bold?: boolean
79-
/** Set text color (named colors or hex values) */
92+
/** Set text color (named colors like 'red', hex like '#FF0000', or ANSI 256 codes like 'ansi:123' or '196') */
8093
color?: string
8194
/** Apply dim/faded styling to text (maps to light weight) */
8295
dimColor?: boolean
@@ -144,6 +157,95 @@ export interface BorderEdges {
144157
top?: boolean
145158
}
146159

160+
/**
161+
* Custom border characters for completely custom border rendering.
162+
*
163+
* @example
164+
* ```typescript
165+
* Box({
166+
* customBorderChars: {
167+
* topLeft: '╔',
168+
* topRight: '╗',
169+
* bottomLeft: '╚',
170+
* bottomRight: '╝',
171+
* top: '═',
172+
* bottom: '═',
173+
* left: '║',
174+
* right: '║'
175+
* }
176+
* })
177+
* ```
178+
*/
179+
export interface CustomBorderChars {
180+
/** Bottom border character */
181+
bottom: string
182+
/** Bottom-left corner character */
183+
bottomLeft: string
184+
/** Bottom-right corner character */
185+
bottomRight: string
186+
/** Left border character */
187+
left: string
188+
/** Right border character */
189+
right: string
190+
/** Top border character */
191+
top: string
192+
/** Top-left corner character */
193+
topLeft: string
194+
/** Top-right corner character */
195+
topRight: string
196+
}
197+
198+
/**
199+
* Border style for Box/View components.
200+
*
201+
* @example
202+
* ```typescript
203+
* Box({ borderStyle: 'single' }) // ┌──┐
204+
* Box({ borderStyle: 'double' }) // ╔══╗
205+
* Box({ borderStyle: 'rounded' }) // ╭──╮
206+
* Box({ borderStyle: 'bold' }) // ┏━━┓
207+
* Box({ borderStyle: 'double-left-right' }) // ╓──╖
208+
* Box({ borderStyle: 'double-top-bottom' }) // ╒══╕
209+
* Box({ borderStyle: 'classic' }) // +--+
210+
* ```
211+
*/
212+
export type BorderStyle =
213+
| 'none'
214+
| 'single'
215+
| 'double'
216+
| 'rounded'
217+
| 'bold'
218+
| 'double-left-right'
219+
| 'double-top-bottom'
220+
| 'classic'
221+
222+
/**
223+
* Mixed text content with individual styling per section.
224+
*
225+
* @example
226+
* ```typescript
227+
* {
228+
* text: 'Error:',
229+
* color: 'red',
230+
* weight: 'bold',
231+
* decoration: 'underline',
232+
* italic: false
233+
* }
234+
* ```
235+
*/
236+
export interface MixedTextContentSection {
237+
/** Text color (named colors, hex, or ANSI codes) */
238+
color?: string
239+
/** Text decoration (underline, strikethrough, or none) */
240+
decoration?: 'underline' | 'strikethrough' | 'none'
241+
/** Apply italic styling */
242+
italic?: boolean
243+
/** The text content for this section */
244+
text: string
245+
/** Text weight (normal, bold, or light) */
246+
weight?: TextWeight
247+
}
248+
147249
/**
148250
* Box/View layout properties (flexbox).
149251
*
@@ -186,14 +288,16 @@ export interface BoxProps {
186288
alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch'
187289
/** Background color (named colors or hex) */
188290
backgroundColor?: string
189-
/** Border color (named colors or hex) */
291+
/** Border color (named colors, hex, or ANSI codes like 'ansi:123' or '196') */
190292
borderColor?: string
191293
/** Configure which border edges to render */
192294
borderEdges?: BorderEdges
193-
/** Border style (none, single, double, rounded, bold) */
194-
borderStyle?: 'single' | 'double' | 'rounded' | 'bold' | 'none'
295+
/** Border style (supports all variants including double-left-right, double-top-bottom, classic) */
296+
borderStyle?: BorderStyle
195297
/** Bottom inset for absolute positioning (can be negative) */
196298
bottom?: number
299+
/** Custom border characters (when using custom border style) */
300+
customBorderChars?: CustomBorderChars
197301
/** Child elements to render inside this box */
198302
children?: Element | Element[]
199303
/** Gap between columns in flex layout */
@@ -312,6 +416,46 @@ export interface TextProps extends TextStyle {
312416
wrap?: TextWrap
313417
}
314418

419+
/**
420+
* Mixed text properties for rendering text with multiple styles.
421+
*
422+
* @example
423+
* ```typescript
424+
* MixedText({
425+
* contents: [
426+
* { text: 'Error: ', color: 'red', weight: 'bold' },
427+
* { text: 'File not found', color: 'white', italic: true }
428+
* ]
429+
* })
430+
* ```
431+
*/
432+
export interface MixedTextProps {
433+
/** Horizontal text alignment */
434+
align?: TextAlign
435+
/** Array of text sections with individual styling */
436+
contents: MixedTextContentSection[]
437+
/** Text wrapping behavior */
438+
wrap?: TextWrap
439+
}
440+
441+
/**
442+
* Fragment properties for grouping elements without layout impact.
443+
*
444+
* @example
445+
* ```typescript
446+
* Fragment({
447+
* children: [
448+
* Text({ children: 'Line 1' }),
449+
* Text({ children: 'Line 2' })
450+
* ]
451+
* })
452+
* ```
453+
*/
454+
export interface FragmentProps {
455+
/** Child elements to group */
456+
children: Element | Element[]
457+
}
458+
315459
/**
316460
* Element type (iocraft element).
317461
*/
@@ -530,6 +674,18 @@ export function Box(props: BoxProps): Element {
530674
if (props.borderStyle) {
531675
node.border_style = props.borderStyle
532676
}
677+
if (props.customBorderChars) {
678+
node.custom_border_chars = {
679+
top_left: props.customBorderChars.topLeft,
680+
top_right: props.customBorderChars.topRight,
681+
bottom_left: props.customBorderChars.bottomLeft,
682+
bottom_right: props.customBorderChars.bottomRight,
683+
top: props.customBorderChars.top,
684+
bottom: props.customBorderChars.bottom,
685+
left: props.customBorderChars.left,
686+
right: props.customBorderChars.right,
687+
}
688+
}
533689

534690
// Background
535691
if (props.backgroundColor) {
@@ -539,6 +695,70 @@ export function Box(props: BoxProps): Element {
539695
return node
540696
}
541697

698+
/**
699+
* Create a mixed text element with multiple styled sections.
700+
*
701+
* @example
702+
* ```typescript
703+
* MixedText({
704+
* contents: [
705+
* { text: 'Success: ', color: 'green', weight: 'bold' },
706+
* { text: 'Operation completed', color: 'white' }
707+
* ],
708+
* align: 'center'
709+
* })
710+
* ```
711+
*/
712+
export function MixedText(props: MixedTextProps): Element {
713+
const node: Element = {
714+
type: 'MixedText',
715+
mixed_text_contents: props.contents.map((section) => ({
716+
text: section.text,
717+
color: section.color,
718+
weight: section.weight,
719+
decoration: section.decoration,
720+
italic: section.italic,
721+
})),
722+
}
723+
724+
if (props.align) {
725+
node.align = props.align
726+
}
727+
if (props.wrap) {
728+
node.wrap = props.wrap
729+
}
730+
731+
return node
732+
}
733+
734+
/**
735+
* Create a fragment element that groups children without layout impact.
736+
*
737+
* Fragments are transparent wrappers that allow returning multiple elements
738+
* without affecting the layout hierarchy.
739+
*
740+
* @example
741+
* ```typescript
742+
* Fragment({
743+
* children: [
744+
* Text({ children: 'Line 1' }),
745+
* Text({ children: 'Line 2' }),
746+
* Text({ children: 'Line 3' })
747+
* ]
748+
* })
749+
* ```
750+
*/
751+
export function Fragment(props: FragmentProps): Element {
752+
const children = Array.isArray(props.children)
753+
? props.children
754+
: [props.children]
755+
756+
return {
757+
type: 'Fragment',
758+
children,
759+
}
760+
}
761+
542762
/**
543763
* Render an element to a string.
544764
*/

0 commit comments

Comments
 (0)