@unblessed/react
Version:
React renderer for unblessed with flexbox layout support
610 lines (595 loc) • 16.2 kB
text/typescript
import * as react from 'react';
import { ReactNode } from 'react';
import { FlexboxProps } from '@unblessed/layout';
import { MouseEvent, KeyEvent, Runtime, Screen } from '@unblessed/core';
import * as react_jsx_runtime from 'react/jsx-runtime';
/**
* types.ts - Type definitions for @unblessed/react
*/
/**
* Options for the render() function
*/
interface RenderOptions {
/**
* Runtime instance
* Create this from @unblessed/node or @unblessed/browser
*/
runtime: Runtime;
/**
* Screen instance (optional)
* If not provided, render() will create a default Screen.
* If provided, you are responsible for calling screen.destroy().
*/
screen?: Screen;
/**
* Debug mode - logs render cycles
*/
debug?: boolean;
}
/**
* Instance returned by render()
*/
interface RenderInstance {
/**
* The screen instance.
*/
screen: Screen;
/**
* Unmount the React tree and clean up
*/
unmount: () => void;
/**
* Re-render with new element
*/
rerender: (element: ReactNode) => void;
/**
* Wait for exit (Promise that resolves when unmounted)
*/
waitUntilExit: () => Promise<void>;
}
/**
* Event handler map for wiring React props to unblessed widget events
*/
interface EventHandlers {
click?: (data: MouseEvent) => void;
mousedown?: (data: MouseEvent) => void;
mouseup?: (data: MouseEvent) => void;
mousemove?: (data: MouseEvent) => void;
mouseover?: (data: MouseEvent) => void;
mouseout?: (data: MouseEvent) => void;
mousewheel?: (data: MouseEvent) => void;
wheeldown?: (data: MouseEvent) => void;
wheelup?: (data: MouseEvent) => void;
keypress?: (ch: string, key: KeyEvent) => void;
focus?: () => void;
blur?: () => void;
press?: () => void;
submit?: (value?: string) => void;
cancel?: () => void;
action?: () => void;
}
/**
* React event props that map to unblessed widget events
*/
interface ReactEventProps {
onClick?: (data: MouseEvent) => void;
onMouseDown?: (data: MouseEvent) => void;
onMouseUp?: (data: MouseEvent) => void;
onMouseMove?: (data: MouseEvent) => void;
onMouseOver?: (data: MouseEvent) => void;
onMouseOut?: (data: MouseEvent) => void;
onMouseWheel?: (data: MouseEvent) => void;
onWheelDown?: (data: MouseEvent) => void;
onWheelUp?: (data: MouseEvent) => void;
onKeyPress?: (ch: string, key: KeyEvent) => void;
onFocus?: () => void;
onBlur?: () => void;
onPress?: () => void;
onSubmit?: (value?: string) => void;
onCancel?: () => void;
onAction?: () => void;
}
/**
* common-props.ts - Shared prop interfaces for widget inheritance
*
* This module defines common prop interfaces that are composed/extended
* by specific widget descriptors, avoiding duplication.
*/
/**
* Props for widgets that can receive focus
*/
interface FocusableProps {
/**
* Tab index for focus navigation (0 = can be focused, -1 = skip)
* @default 0 for interactive widgets
*/
tabIndex?: number;
}
/**
* Props for widgets with borders
*/
interface BorderProps {
borderStyle?: "single" | "double" | "round" | "bold" | "classic";
borderColor?: string;
borderDimColor?: boolean;
borderTopColor?: string;
borderBottomColor?: string;
borderLeftColor?: string;
borderRightColor?: string;
borderTopDim?: boolean;
borderBottomDim?: boolean;
borderLeftDim?: boolean;
borderRightDim?: boolean;
}
/**
* Props for interactive widgets (buttons, inputs, etc.)
* Combines layout, events, focus behavior, and styling
* Default state style properties are direct props (inherited from StyleObject)
* State variations use nested objects (hover, focus)
*/
interface InteractiveWidgetProps extends FlexboxProps, ReactEventProps, FocusableProps, BorderProps, Omit<StyleObject, "border"> {
hover?: StyleObject;
focus?: StyleObject;
}
/**
* Border state styling object
* Used for styling borders in different states (hover, focus, etc.)
*/
interface BorderStyleObject {
color?: string;
backgroundColor?: string;
fg?: string;
bg?: string;
background?: string;
topColor?: string;
bottomColor?: string;
leftColor?: string;
rightColor?: string;
dim?: boolean;
topDim?: boolean;
bottomDim?: boolean;
leftDim?: boolean;
rightDim?: boolean;
}
/**
* Complete style object for widget state styling
* Supports both primary names and shorthands (like CSS)
*/
interface StyleObject {
color?: string;
backgroundColor?: string;
fg?: string;
bg?: string;
background?: string;
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
reverse?: boolean;
dim?: boolean;
blink?: boolean;
hide?: boolean;
border?: BorderStyleObject;
}
/**
* Props interface for Box component
* Combines flexbox layout props with box-specific visual properties
*
* Inherits all interactive widget properties including:
* - Layout (flexbox, width, height, padding, margin, etc.)
* - Events (onClick, onKeyPress, onFocus, etc.)
* - Styling (color, bg, bold, etc.)
* - State styling (hover, focus)
* - Borders (borderStyle, borderColor, etc.)
*/
interface BoxProps extends InteractiveWidgetProps {
tags?: boolean;
content?: string;
children?: ReactNode;
}
/**
* Box component - Container with flexbox layout support
*
* Supports flexbox properties, borders, colors, event handling, and state styling.
* Default state styling uses direct props (color, bg, bold, etc.)
* State variations use nested objects (hover, focus)
*
* @example Basic layout
* ```tsx
* <Box
* flexDirection="row"
* gap={2}
* padding={1}
* border={1}
* borderStyle="single"
* borderColor="cyan"
* >
* <Box width={20}>Left</Box>
* <Box flexGrow={1}>Middle</Box>
* <Box width={20}>Right</Box>
* </Box>
* ```
*
* @example With styling and state effects
* ```tsx
* <Box
* padding={1}
* border={1}
* borderStyle="single"
* borderColor="white"
* color="gray"
* hover={{ border: { color: "cyan" }, color: "white" }}
* focus={{ border: { color: "yellow" } }}
* >
* Interactive Box
* </Box>
* ```
*
* @example With event handling
* ```tsx
* <Box
* padding={1}
* border={1}
* borderStyle="single"
* tabIndex={0}
* onClick={(data) => console.log('Clicked!', data)}
* onKeyPress={(ch, key) => {
* if (key.name === 'enter') handleSubmit();
* }}
* >
* Click or press Enter
* </Box>
* ```
*/
declare const Box: react.ForwardRefExoticComponent<BoxProps & {
children?: ReactNode | undefined;
} & react.RefAttributes<any>>;
/**
* Props interface for Button component
* Inherits all interactive widget properties (layout, events, focus, borders, styling)
*/
interface ButtonProps extends InteractiveWidgetProps {
content?: string;
children?: ReactNode;
}
/**
* Button component - Interactive button with hover and focus effects
*
* Supports mouse clicks, keyboard press (Enter), and visual state changes.
* Automatically receives focus when tabbed to.
*
* Default state styling uses direct props (color, bg, bold, etc.)
* State variations use nested objects (hover, focus)
*
* @example Basic button
* ```tsx
* <Button
* borderStyle="single"
* borderColor="green"
* padding={1}
* onClick={() => console.log('Clicked!')}
* >
* Click Me
* </Button>
* ```
*
* @example With hover and focus effects
* ```tsx
* <Button
* borderStyle="single"
* borderColor="blue"
* color="white"
* bg="blue"
* bold={true}
* hover={{ bg: "darkblue" }}
* focus={{ border: { color: "cyan" } }}
* padding={1}
* onPress={() => handleSubmit()}
* >
* Submit
* </Button>
* ```
*
* @example Interactive counter
* ```tsx
* const [count, setCount] = useState(0);
*
* <Button onClick={() => setCount(c => c + 1)}>
* Count: {count}
* </Button>
* ```
*/
declare const Button: react.ForwardRefExoticComponent<ButtonProps & {
children?: ReactNode | undefined;
} & react.RefAttributes<any>>;
/**
* Props interface for Input component
* Inherits all interactive widget properties (layout, events, focus, borders)
*/
interface InputProps extends InteractiveWidgetProps {
/** Controlled value - when provided, input becomes controlled */
value?: string;
/** Default value for uncontrolled mode */
defaultValue?: string;
}
/**
* Input component - Text input field for user interaction
*
* Provides a single-line text input with submit/cancel events.
* Users can type text and submit with Enter or cancel with Escape.
* Automatically receives focus when tabbed to (tabIndex=0 by default).
*
* Supports both controlled and uncontrolled modes:
* - Controlled: Provide `value` prop and update it via events
* - Uncontrolled: Use `defaultValue` for initial value, or omit for empty input
*
* Default state styling uses direct props (color, bg, bold, etc.)
* State variations use nested objects (hover, focus)
*
* @example Uncontrolled input (with default value)
* ```tsx
* <Input
* width={30}
* defaultValue="Initial text"
* color="white"
* focus={{ border: { color: "cyan" } }}
* onSubmit={(value) => console.log('Submitted:', value)}
* onCancel={() => console.log('Cancelled')}
* />
* ```
*
* @example Controlled input
* ```tsx
* const [text, setText] = useState('');
* <Input
* width={40}
* value={text}
* onChange={(newValue) => setText(newValue)}
* onSubmit={(value) => handleSubmit(value)}
* />
* ```
*
* @example With auto-focus and styling
* ```tsx
* <Input
* width={40}
* defaultValue="Focus me!"
* color="white"
* bg="blue"
* focus={{ border: { color: "cyan" }, bg: "darkblue" }}
* onSubmit={(value) => handleSubmit(value)}
* />
* ```
*/
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<any>>;
/**
* Props interface for List component
* Provides a simplified, React-friendly API with sensible defaults
*
* Styling hierarchy:
* - style, hover, focus: Affect the List container (borders, background)
* - itemStyle, itemSelected, itemHover: Affect individual list items
*/
interface ListProps extends InteractiveWidgetProps {
items: string[];
children?: ReactNode;
label: string;
onSelect?: (item: string, index: number) => void;
onCancel?: () => void;
defaultSelected?: number;
disabled?: boolean;
vi?: boolean;
itemStyle?: StyleObject;
itemSelected?: StyleObject;
itemHover?: StyleObject;
scrollbar?: boolean;
scrollbarBg?: string;
scrollbarFg?: string;
scrollbarChar?: string;
scrollbarTrack?: boolean;
scrollbarTrackBg?: string;
scrollbarTrackFg?: string;
scrollbarTrackChar?: string;
}
/**
* List component - Scrollable, selectable list with keyboard and mouse support
*
* Provides a simplified API with better defaults than raw unblessed List.
* Mouse, keyboard, and tags are always enabled.
*
* Styling hierarchy:
* - style, hover, focus: Affect the List container (borders, background)
* - itemStyle, itemSelected, itemHover: Affect individual list items
*
* @example Basic list
* ```tsx
* <List
* items={['Apple', 'Banana', 'Cherry']}
* label="Choose a fruit"
* width={40}
* height={10}
* border={1}
* borderStyle="single"
* itemSelected={{ bg: "blue", color: "white" }}
* onSelect={(item, index) => console.log('Selected:', item)}
* />
* ```
*
* @example With list container and item styling
* ```tsx
* <List
* items={items}
* label="Select an option"
* border={1}
* borderStyle="single"
* borderColor="white"
* hover={{ border: { color: "cyan" } }} // List border on hover
* focus={{ border: { color: "yellow" } }} // List border on focus
* itemStyle={{ color: "gray" }} // Normal items
* itemSelected={{ bg: "white", color: "black", bold: true }} // Selected item
* itemHover={{ bg: "darkgray" }} // Hovered item
* />
* ```
*
* @example Disabled list (read-only)
* ```tsx
* <List
* items={items}
* label="Read only"
* disabled={true}
* itemStyle={{ color: "gray" }}
* />
* ```
*/
declare const List: react.ForwardRefExoticComponent<ListProps & react.RefAttributes<any>>;
/**
* Props interface for Spacer component
*/
interface SpacerProps extends FlexboxProps {
}
/**
* Spacer component - Flexible space that expands to fill available space
*
* This is a convenience component that renders a Box with flexGrow={1}.
* It's useful for creating space-between layouts.
*
* @example
* ```tsx
* <Box flexDirection="row">
* <Box width={20}>Left</Box>
* <Spacer />
* <Box width={20}>Right</Box>
* </Box>
* ```
*
* @example With custom flexGrow
* ```tsx
* <Box flexDirection="row">
* <Spacer flexGrow={2} />
* <Box width={30}>Content</Box>
* <Spacer flexGrow={1} />
* </Box>
* ```
*/
declare const Spacer: {
(props: SpacerProps): react_jsx_runtime.JSX.Element;
displayName: string;
};
/**
* Props interface for Text component
*/
interface TextProps extends StyleObject {
content?: string;
children?: ReactNode;
}
/**
* Text component - Renders text with styling
*
* @example
* ```tsx
* <Text color="green" bold>
* Hello World!
* </Text>
* ```
*
* @example With nested text
* ```tsx
* <Text>
* Hello <Text color="red">World</Text>!
* </Text>
* ```
*/
declare const Text: react.ForwardRefExoticComponent<TextProps & {
children?: ReactNode | undefined;
} & react.RefAttributes<any>>;
/**
* Props interface for BigText component
*/
interface BigTextProps extends FlexboxProps, Pick<StyleObject, "color" | "backgroundColor"> {
font?: string;
fontBold?: boolean;
char?: string;
content?: string;
children?: string;
}
/**
* BigText component - Renders large ASCII art text
*
* Uses terminal fonts to render large text. Each character is 14 rows × 8 columns.
* Supports all BoxProps including flexbox layout, borders, and event handling.
*
* @example
* ```tsx
* <BigText color="cyan">
* HELLO
* </BigText>
* ```
*
* @example With border and events
* ```tsx
* <BigText
* color="green"
* borderStyle="single"
* padding={1}
* onClick={() => console.log('Big text clicked!')}
* >
* WELCOME
* </BigText>
* ```
*/
declare const BigText: react.ForwardRefExoticComponent<BigTextProps & {
children?: react.ReactNode | undefined;
} & react.RefAttributes<any>>;
/**
* render.ts - Main render function for @unblessed/react
*
* This module provides the main `render()` function that users call to
* mount React components to an unblessed Screen.
*/
/**
* Render a React element to an unblessed Screen
*
* @example
* Basic usage (render() creates screen automatically):
* ```tsx
* import { NodeRuntime } from '@unblessed/node';
* import { render, Box, Text } from '@unblessed/react';
*
* const App = () => (
* <Box flexDirection="row" gap={2}>
* <Box width={20}>Left</Box>
* <Box flexGrow={1}>Middle</Box>
* <Box width={20}>Right</Box>
* </Box>
* );
*
* const instance = render(<App />, { runtime: new NodeRuntime() });
*
* // Later:
* instance.unmount(); // Automatically destroys screen
* ```
*
* @example
* Advanced usage (provide custom screen):
* ```tsx
* import { Screen, NodeRuntime } from '@unblessed/node';
* import { render, Box, Text } from '@unblessed/react';
*
* const screen = new Screen({
* smartCSR: false,
* debug: true,
* log: './debug.log'
* });
*
* const instance = render(<App />, {
* runtime: new NodeRuntime(),
* screen
* });
*
* // Later:
* instance.unmount();
* screen.destroy(); // You must destroy screen manually
* ```
*/
declare function render(element: ReactNode, options: RenderOptions): RenderInstance;
export { BigText, type BigTextProps, Box, type BoxProps, Button, type ButtonProps, type EventHandlers, Input, type InputProps, List, type ListProps, type ReactEventProps, type RenderInstance, type RenderOptions, Spacer, Text, type TextProps, render };