UNPKG

tuix

Version:

A performant TUI framework for Bun with JSX and reactive state management

337 lines (287 loc) 9.63 kB
/** * JSX Runtime for CLI-KIT * * Enables JSX/TSX syntax for building terminal UIs * Compatible with React JSX transform with Svelte-inspired binding support */ import type { View } from "./core/types" import { text, vstack, hstack, styledText } from "./core/view" import { style, type Style, Colors } from "./styling" import type { ComponentProps } from "./components/component" import { isBindableRune, isStateRune, type BindableRune, type StateRune } from './reactivity/runes' // JSX namespace for TypeScript export namespace JSX { export interface Element extends View {} export interface IntrinsicElements { // Text elements text: TextProps span: TextProps // Layout elements vstack: StackProps hstack: StackProps div: StackProps // Styled elements bold: TextProps italic: TextProps underline: TextProps faint: TextProps // Color elements red: TextProps green: TextProps blue: TextProps yellow: TextProps cyan: TextProps magenta: TextProps white: TextProps gray: TextProps // Semantic elements error: TextProps success: TextProps warning: TextProps info: TextProps // Components panel: PanelProps button: ButtonProps list: ListProps input: InputProps } export interface TextProps { children?: string | number | boolean | null | undefined style?: Style color?: keyof typeof Colors bold?: boolean italic?: boolean underline?: boolean faint?: boolean } export interface StackProps { children?: JSX.Element | JSX.Element[] | string | (JSX.Element | string | null | undefined | false)[] gap?: number align?: "start" | "center" | "end" justify?: "start" | "center" | "end" | "between" | "around" } export interface PanelProps { children?: JSX.Element | JSX.Element[] title?: string border?: "single" | "double" | "rounded" | "thick" padding?: number width?: number height?: number } export interface ButtonProps extends TextProps { onClick?: () => void variant?: "primary" | "secondary" | "success" | "danger" disabled?: boolean } export interface ListProps { items: Array<{ id: string; label: string | JSX.Element }> selected?: number onSelect?: (index: number) => void } export interface InputProps { value?: string placeholder?: string onChange?: (value: string) => void type?: "text" | "password" 'bind:value'?: BindableRune<string> | StateRune<string> } // Support bind: syntax on all intrinsic elements export interface IntrinsicAttributes { [key: `bind:${string}`]: any } } // Helper to flatten and filter children function normalizeChildren(children: any): View[] { if (!children) return [] const flatten = (arr: any): any[] => { return arr.reduce((flat: any[], item: any) => { if (Array.isArray(item)) { return flat.concat(flatten(item)) } if (item === null || item === undefined || item === false) { return flat } return flat.concat(item) }, []) } const normalized = Array.isArray(children) ? flatten(children) : [children] return normalized.map(child => { if (typeof child === 'string' || typeof child === 'number') { return text(String(child)) } return child }) } // Create text with style function createStyledText(content: string, styleBuilder: Style): View { return styledText(content, styleBuilder) } /** * Process bind: props for two-way data binding */ function processBindProps(props: any): any { if (!props) return props const processed = { ...props } for (const [key, value] of Object.entries(props)) { if (key.startsWith('bind:')) { const bindProp = key.slice(5) // Remove 'bind:' prefix const capitalizedProp = bindProp.charAt(0).toUpperCase() + bindProp.slice(1) delete processed[key] if (isBindableRune(value) || isStateRune(value)) { // It's a rune - set up two-way binding processed[bindProp] = value() // Current value processed[`on${capitalizedProp}Change`] = (newValue: any) => { value.$set(newValue) } // If it's bindable, we can also pass the rune itself for advanced use if (isBindableRune(value)) { processed[`${bindProp}Rune`] = value } } else { // It's a regular variable - just pass the value // (can't do two-way binding without a rune) processed[bindProp] = value } } } return processed } // JSX Factory function export function jsx( type: string | Function, props: any, key?: string ): JSX.Element { // Process bind: props before anything else const processedProps = processBindProps(props) // Handle function components if (typeof type === 'function') { return type(processedProps || {}) } const { children, ...restProps } = processedProps || {} // Handle intrinsic elements switch (type) { // Text elements case 'text': case 'span': { // Handle children array properly let content = '' if (Array.isArray(children)) { content = children.join('') } else if (children != null) { content = String(children) } let textStyle = restProps.style || style() if (restProps.color) { textStyle = textStyle.foreground(Colors[restProps.color]) } if (restProps.bold) textStyle = textStyle.bold() if (restProps.italic) textStyle = textStyle.italic() if (restProps.underline) textStyle = textStyle.underline() if (restProps.faint) textStyle = textStyle.faint() return content ? createStyledText(content, textStyle) : text('') } // Layout elements case 'vstack': case 'div': { const views = normalizeChildren(children) return views.length === 0 ? text('') : vstack(...views) } case 'hstack': { const views = normalizeChildren(children) return views.length === 0 ? text('') : hstack(...views) } // Styled text shortcuts case 'bold': { const content = Array.isArray(children) ? children.join('') : String(children || '') return createStyledText(content, style().bold()) } case 'italic': { const content = Array.isArray(children) ? children.join('') : String(children || '') return createStyledText(content, style().italic()) } case 'underline': { const content = Array.isArray(children) ? children.join('') : String(children || '') return createStyledText(content, style().underline()) } case 'faint': { const content = Array.isArray(children) ? children.join('') : String(children || '') return createStyledText(content, style().faint()) } // Color shortcuts case 'red': case 'green': case 'blue': case 'yellow': case 'cyan': case 'magenta': case 'white': case 'gray': { const content = Array.isArray(children) ? children.join('') : String(children || '') const color = Colors[type as keyof typeof Colors] return createStyledText(content, style().foreground(color)) } // Semantic elements case 'error': case 'success': case 'warning': case 'info': { const content = Array.isArray(children) ? children.join('') : String(children || '') const colorMap = { error: Colors.red, success: Colors.green, warning: Colors.yellow, info: Colors.blue } return createStyledText(content, style().foreground(colorMap[type as keyof typeof colorMap]).bold()) } // Components (these would need to be imported and implemented) case 'panel': { // Lazy import to avoid circular dependencies const { Panel } = require('./components/builders/Panel') const views = normalizeChildren(children) return Panel(views.length === 1 ? views[0] : vstack(...views), restProps) } case 'button': { const { Button, PrimaryButton, SecondaryButton, SuccessButton, DangerButton } = require('./components/builders/Button') const content = String(children || '') switch (restProps.variant) { case 'primary': return PrimaryButton(content, restProps) case 'secondary': return SecondaryButton(content, restProps) case 'success': return SuccessButton(content, restProps) case 'danger': return DangerButton(content, restProps) default: return Button(content, restProps) } } default: // Unknown element type - return a text element as fallback console.warn(`Unknown JSX element type: ${type}, falling back to text`) let content = '' if (Array.isArray(children)) { content = children.join('') } else if (children != null) { content = String(children) } return text(content) } } // JSX Fragment export function Fragment(props: { children?: any }): JSX.Element { const views = normalizeChildren(props.children) return views.length === 0 ? text('') : vstack(...views) } // Automatic runtime exports (for React 17+ JSX transform) export { jsx as jsxs, jsx as jsxDEV } // Classic runtime exports export function createElement( type: string | Function, props: any, ...children: any[] ): JSX.Element { return jsx(type, { ...props, children: children.length === 1 ? children[0] : children }) }