asciitorium
Version:
an ASCII CLUI framework
270 lines (269 loc) • 10.4 kB
TypeScript
import { Alignment, SizeValue, SizeContext, ComponentStyle, GapValue, PositionValue } from './types.js';
import type { State } from './State.js';
import { LayoutType, LayoutOptions } from './layouts/Layout.js';
/**
* Configuration interface for Component initialization.
* Supports both individual style properties and consolidated style objects.
*/
export interface ComponentProps {
/** Optional label displayed at the top of the component */
label?: string;
/** Optional comment for documentation purposes (not displayed) */
comment?: string;
/** Whether to display the label when provided */
showLabel?: boolean;
/** Consolidated style properties object */
style?: ComponentStyle;
/** Component width (number, percentage string, or 'auto') */
width?: SizeValue;
/** Component height (number, percentage string, or 'auto') */
height?: SizeValue;
/** Whether to render a border around the component */
border?: boolean;
/** Character used for component background */
background?: string;
/** Groups and aligns all children within this component (Row/Column containers only) */
align?: Alignment;
/** Exact coordinate placement - overrides layout positioning */
position?: PositionValue;
/** Spacing around the component */
gap?: GapValue;
/** State binding for reactive updates (deprecated) */
bind?: State<any> | ((state: State<any>) => void);
/** Child components to be managed by this component */
children?: Component[];
/** Layout algorithm to use for positioning children */
layout?: LayoutType;
/** Configuration options for the selected layout */
layoutOptions?: LayoutOptions;
/**
* Dynamic content switching system that allows runtime swapping of child components
* based on a reactive state key. Supports component instances, classes, or factory functions.
*/
dynamicContent?: {
/** State that determines which component to display */
selectedKey: State<string>;
/** Map of keys to components (instances, classes, or factories) */
componentMap: Record<string, Component | (() => Component) | (new (...args: any[]) => Component)>;
/** Fallback component when selectedKey doesn't match any map entry */
fallback?: Component | (() => Component) | (new (...args: any[]) => Component);
};
/** Hotkey for quick access to this component. If not provided, one will be auto-assigned. */
hotkey?: string;
/** Whether the component is visible (default: true). Must be a State object for reactive binding. */
visible?: State<boolean>;
}
/**
* Abstract base class for all UI components in the asciitorium framework.
*
* Provides core functionality including:
* - **Position and size management**: Supports absolute sizing, percentages, 'auto', and 'fill'
* - **Child component management**: Automatic layout calculation via Row/Column layouts
* - **Focus handling**: Visual indicators and keyboard navigation support
* - **State binding**: Reactive updates when bound state changes
* - **ASCII-based rendering**: Character-based 2D buffers with transparency
* - **Dynamic content switching**: Runtime component replacement based on state
*
* ## Rendering System
*
* Components use a character-based rendering system where each component
* renders to a 2D string array buffer. The transparent character '‽' allows
* for overlay effects and complex compositions.
*
* ## Focus System
*
* Focus-enabled components automatically switch between single-line borders
* (╭╮╰╯─│) and double-line borders (╔╗╚╝═║) when focused.
*
* ## Layout System
*
* Components can be positioned in two ways:
* 1. **Relative positioning**: Using Row/Column layouts (default)
* 2. **Absolute positioning**: Using the `position` prop with x/y/z coordinates
*
* @example
* Creating a custom component:
* ```typescript
* class MyComponent extends Component {
* constructor(options: ComponentProps) {
* super({
* ...options,
* width: options.width ?? 20,
* height: options.height ?? 10,
* border: true
* });
* }
*
* override draw(): string[][] {
* // Custom rendering logic
* this.buffer = Array.from({ length: this.height }, () =>
* Array.from({ length: this.width }, () => ' ')
* );
* return this.buffer;
* }
* }
* ```
*
* @example
* Using component props with JSX:
* ```tsx
* <Button
* width="50%"
* height={5}
* border
* align="center"
* position={{ x: 10, y: 5, z: 100 }}
* gap={{ x: 2, y: 1 }}
* >
* Click Me
* </Button>
* ```
*/
export declare abstract class Component {
/** Optional label displayed at the top of the component */
label: string | undefined;
/** Optional comment for documentation (not rendered) */
comment: string | undefined;
/** Whether to display the label when provided */
showLabel: boolean;
/** Current rendered width in characters */
width: number;
/** Current rendered height in characters */
height: number;
/** Whether to render a border around the component */
border: boolean;
/** Character used to fill the component background */
fill: string;
/** Groups and aligns all children within this component (Row/Column containers only) */
align?: Alignment;
/** Whether the component uses fixed positioning */
fixed: boolean;
/** Absolute X position */
x: number;
/** Absolute Y position */
y: number;
/** Z-index for rendering order (higher values on top) */
z: number;
/** Spacing around the component */
gap: GapValue;
/** Whether this component can receive keyboard focus */
focusable: boolean;
/** Whether this component currently has focus */
hasFocus: boolean;
/**
* When true, component captures ALL keyboard input except bypass keys.
* Used by input components (TextInput, etc.) to receive all keystrokes.
*/
captureModeActive: boolean;
/** Keys that bypass capture mode and always work for navigation */
static readonly BYPASS_KEYS: string[];
/** Character used for transparency in rendering ('‽' allows overlays) */
transparentChar: string;
/** Reference to parent component in the hierarchy */
parent?: Component;
/** Hotkey assigned to this component for quick access */
hotkey?: string;
/** State object controlling component visibility */
private visibleState?;
/** Original size values for relative sizing calculations */
protected originalWidth?: SizeValue;
protected originalHeight?: SizeValue;
/** Current render buffer (2D character array) */
protected buffer: string[][];
/** Cleanup functions for state subscriptions */
private unbindFns;
/** Cleanup functions registered via registerCleanup() */
private cleanupFns;
/** Child components managed by this component */
protected children: Component[];
/** Layout algorithm used for positioning children */
protected layoutType: LayoutType;
/** Configuration for the layout algorithm */
protected layoutOptions?: LayoutOptions;
/** Cached layout instance for performance */
private layout?;
/**
* Initializes a new Component with the provided properties.
*
* @param props Configuration object containing style, layout, and behavior options
*/
constructor(props: ComponentProps);
setParent(parent: Component): void;
/**
* Gets the current visibility state of the component.
* @returns True if the component is visible (default), false if explicitly hidden
*/
get visible(): boolean;
/**
* Initializes child components from props and sets up layout.
*
* @param props Component properties containing potential children
*/
private initializeChildren;
/**
* Validates that a potential child is a valid Component.
*
* @param child Potential child component to validate
* @returns True if the child is a valid Component
*/
private isValidChild;
addChild(child: Component): void;
removeChild(child: Component): void;
getChildren(): Component[];
setChildren(children: Component[]): void;
getAllDescendants(): Component[];
protected invalidateLayout(): void;
protected recalculateLayout(): void;
protected recalculateAutoSize(): void;
bind<T>(state: State<T>, apply: (val: T) => void): void;
/**
* Registers a cleanup function to be called when the component is destroyed.
* Use this for cleaning up timers, intervals, event listeners, and other resources.
*
* Example:
* ```typescript
* const intervalId = setInterval(() => {...}, 1000);
* component.registerCleanup(() => clearInterval(intervalId));
* ```
*
* Note: State subscriptions created via bind() are automatically cleaned up
* and do not need to be registered with registerCleanup().
*
* @param fn Cleanup function to execute on destroy
*/
registerCleanup(fn: () => void): void;
destroy(): void;
private static calculateAutoWidth;
private static calculateAutoHeight;
protected notifyAppOfFocusRefresh(): void;
/**
* Notifies the application's focus manager that the component tree has changed
* and focus needs to be completely reset (e.g., when swapping out child components).
*/
protected notifyAppOfFocusReset(): void;
getOriginalWidth(): SizeValue | undefined;
getOriginalHeight(): SizeValue | undefined;
resolveSize(context: SizeContext): void;
handleEvent(_event: string): boolean;
/**
* Draws the border around the component with diamond corners for focus indication.
*
* @param drawChar Helper function for safe character drawing within bounds
*/
private drawBorder;
/**
* Renders all child components sorted by z-index and composites them into the buffer.
*/
private renderChildren;
/**
* Composites a single child component's buffer into this component's buffer.
*
* @param child The child component to composite
*/
private compositeChildBuffer;
/**
* Check if hotkey visibility is enabled by finding the App's FocusManager
*/
protected isHotkeyVisibilityEnabled(): boolean;
draw(): string[][];
}