From 8da419c57c2e7a7787400716893a6ecb2a7f4c58 Mon Sep 17 00:00:00 2001 From: Rashi Bajpai Date: Wed, 17 Jun 2026 02:02:00 +0530 Subject: [PATCH 1/3] feat(widgets): add Spinner widget with unicode/ascii fallback --- packages/widgets/src/Spinner.ts | 32 ++++++++++++++++++++++++++++++++ packages/widgets/src/index.ts | 2 ++ tests/helpers/Spinner.test.ts | 11 +++++++++++ 3 files changed, 45 insertions(+) create mode 100644 packages/widgets/src/Spinner.ts create mode 100644 tests/helpers/Spinner.test.ts diff --git a/packages/widgets/src/Spinner.ts b/packages/widgets/src/Spinner.ts new file mode 100644 index 00000000..ba257919 --- /dev/null +++ b/packages/widgets/src/Spinner.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from '@termuijs/jsx' +import { caps } from '@termuijs/core' +import { Text } from './Text' + +export interface SpinnerProps { + speed?: number + label?: string + color?: string +} + +const FRAMES_UNICODE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] +const FRAMES_ASCII = ['|', '/', '-', '\\'] + +export function Spinner({ speed = 80, label = '', color = 'cyan' }: SpinnerProps) { + const [frame, setFrame] = useState(0) + const frames = caps.unicode ? FRAMES_UNICODE : FRAMES_ASCII + + useEffect(() => { + if (caps.noMotion) return + + const interval = setInterval(() => { + setFrame((f) => (f + 1) % frames.length) + }, speed) + + return () => clearInterval(interval) + }, [speed, frames.length]) + + const spinnerChar = frames[frame] + const display = label ? `${spinnerChar} ${label}` : spinnerChar + + return {display} +} \ No newline at end of file diff --git a/packages/widgets/src/index.ts b/packages/widgets/src/index.ts index 316ba500..87114204 100644 --- a/packages/widgets/src/index.ts +++ b/packages/widgets/src/index.ts @@ -310,3 +310,5 @@ export { UnorderedList } from './display/UnorderedList.js'; export type { UnorderedListOptions } from './display/UnorderedList.js'; export { Rule } from './display/Rule.js'; export type { RuleOrientation, RuleOptions } from './display/Rule.js'; +export { Spinner } from './Spinner' +export type { SpinnerProps } from './Spinner' \ No newline at end of file diff --git a/tests/helpers/Spinner.test.ts b/tests/helpers/Spinner.test.ts new file mode 100644 index 00000000..4cc30e7f --- /dev/null +++ b/tests/helpers/Spinner.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'bun:test' +import { render } from '@termuijs/testing' +import { Spinner } from '../../packages/widgets/src/Spinner' + +describe('Spinner', () => { + it('renders with label', () => { + const t = render() + expect(t.renderToString()).toContain('Loading') + t.unmount() + }) +}) \ No newline at end of file From 51f585c3178e0077758b0cd8011bcf1b41760808 Mon Sep 17 00:00:00 2001 From: Rashi Bajpai Date: Fri, 19 Jun 2026 23:16:26 +0530 Subject: [PATCH 2/3] docs: add explanatory comments to example configurations --- docs/choosing-your-api.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/choosing-your-api.md b/docs/choosing-your-api.md index 188821ce..587db4c8 100644 --- a/docs/choosing-your-api.md +++ b/docs/choosing-your-api.md @@ -33,6 +33,29 @@ The Imperative API is the foundation of TermUI. You work directly with classes a - You need the **absolute lowest overhead** and maximum performance. - You prefer **Object-Oriented Programming** and want to manage state in class properties. +### Common use case: Custom widget + +When building a reusable widget (e.g., a progress bar) for others to import: + +```typescript +import { Widget, Style } from '@termuijs/core' + +export class ProgressBar extends Widget { + private progress: number = 0 + + setProgress(value: number) { + this.progress = Math.max(0, Math.min(1, value)) + this.markDirty() // Request re-render + } + + render(buffer) { + const width = this.rect.width + const filled = Math.round(width * this.progress) + const bar = '█'.repeat(filled) + '░'.repeat(width - filled) + buffer.write(0, 0, bar, Style.fg('green')) + } +} + ### Example: Imperative Counter ```typescript From 4f7016c5e471da2c93e954530bbdcb04448a6ac1 Mon Sep 17 00:00:00 2001 From: Rashi Bajpai Date: Sat, 20 Jun 2026 23:58:46 +0530 Subject: [PATCH 3/3] docs: add JSDoc comments to capability flags module --- packages/core/src/errors.ts | 100 ++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index da04c8f5..cd96a4b0 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -29,3 +29,103 @@ export class TermUIValidationError extends TermUIError { } } +/** + * @fileoverview Runtime capability detection for terminal environments. + * + * TermUI checks the terminal environment at module load to determine + * which features are safe to use. These flags are read-only and + * evaluated once — they do not react to runtime environment changes. + * + * Environment variables: + * - NO_UNICODE=1 → Disable unicode, use ASCII fallbacks + * - NO_MOTION=1 → Skip animations, render static output + * - NO_COLOR=1 → Strip ANSI color codes + * - TERMUI_KEYBINDINGS=vim|emacs → Set navigation mode + * + * @module @termuijs/core/caps + */ + +/** + * Terminal capability flags. + * + * These are evaluated once when the module loads. All built-in widgets + * check these flags automatically. Use them in custom widgets to provide + * graceful fallbacks for restricted terminal environments (e.g., CI, + * screen readers, or user preference). + * + * @example + * ```ts + * import { caps } from '@termuijs/core' + * + * const bullet = caps.unicode ? '●' : '*' // Safe for any terminal + * const bar = caps.unicode ? '█' : '#' // Progress bar fallback + * ``` + */ +export const caps = { + /** + * Whether unicode characters are supported. + * + * Set `NO_UNICODE=1` to disable unicode and force ASCII fallbacks. + * Useful in CI environments or terminals with limited font support. + * + * @default true (when NO_UNICODE is not set) + */ + unicode: !process.env.NO_UNICODE, + + /** + * Whether animations and motion effects are supported. + * + * Set `NO_MOTION=1` to skip all animations and render static output. + * Respects `prefers-reduced-motion` accessibility preference. + * + * @default true (when NO_MOTION is not set) + */ + motion: !process.env.NO_MOTION, + + /** + * Whether ANSI color codes are supported. + * + * Set `NO_COLOR=1` to disable all color output. Useful for: + * - Piping output to files + * - Terminals without color support + * - User preference for plain text + * + * @default true (when NO_COLOR is not set) + */ + color: !process.env.NO_COLOR, + + /** + * Global keybinding navigation mode. + * + * Configures the default keyboard navigation scheme across all widgets. + * + * - `"default"` → Arrow keys (↑↓←→) for navigation + * - `"vim"` → j/k/h/l for navigation (hjkl) + * - `"emacs"` → Ctrl+n/p/f/b for navigation + * + * Set via environment variable: `TERMUI_KEYBINDINGS=vim` + * + * @default "default" + */ + keybindingMode: (process.env.TERMUI_KEYBINDINGS as 'default' | 'vim' | 'emacs') || 'default', +} + +/** + * Checks if the current terminal meets WCAG AA contrast requirements. + * + * This is a convenience helper for theme authors who want to ensure + * accessible color combinations when color support is enabled. + * + * @param foreground - Foreground color in hex, RGB, or named format + * @param background - Background color in hex, RGB, or named format + * @returns true if the combination meets WCAG AA standards + * + * @example + * ```ts + * import { meetsAA } from '@termuijs/core' + * meetsAA('#ffffff', '#000000') // true + * ``` + */ +// If this helper exists in the file, add JSDoc above it: +// export function meetsAA(foreground: string, background: string): boolean { ... } +