TextUI is a terminal UI runtime. A screen is data - a graph of component nodes resolved through late-binding registries - and JSX is a way of writing that data, not a different thing from it.
Read README.md, then docs/: the architecture, the vocabulary, and the rules a consumer has to follow.
The docs are a Jekyll site published to GitHub Pages. There is no Ruby on this
machine and there is not going to be - scripts/docs-serve.sh runs it in a
container, scripts/docs-preview.py serves an already-built _site with no
container at all, and node scripts/check-docs.mjs checks the nav tree without
either. A plain python -m http.server over docs/_site will 404 on every
file, because the site is built with baseurl: /textui and nothing is mounted
there.
Two rules the build enforces: a page's parent: must match another page's
title: exactly, and render_with_liquid: false is what stops Jekyll reading
{{ in a JSX prop as a Liquid variable.
pnpm build # every package (tsc, project references)
pnpm typecheck # every workspace, including components/ and playground/
pnpm test # every suite
pnpm dev <id> # a playground; `--list` for the listPer-package: pnpm --filter @textui/core <script>. Node ≥ 22, pnpm 10.
packages/core/ the runtime and its contracts - published
src/types/ the contracts. Nothing here imports a runtime value
src/core/ store, events, when, and the registries
src/render/ buffer, diff, colour, layout, static rendering
src/runtime/ reconciler, hooks, paint, style resolution
src/jsx/ the JSX factory and runtime
src/themes/ tokens, glyphs, borders, the built-in themes
src/ui/ the component catalog, by category
src/app/ the application: frame loop, input routing, teardown
packages/terminal/ adapters, capabilities, ANSI writing, input decoding
packages/testing/ the headless harness
packages/cli/ the developer CLI and CLI primitives
components/ the source-copy registry - copied into projects
playground/ the showcase and the focused playgrounds
types/is the contract, and it is source-first. Every shape lives insrc/types/and nothing there imports a runtime value. A consumer that has to reach intoruntime/for a type is a contract with a hole in it.- Registries are late-binding. Components, commands, themes, layouts, shells and resource viewers are resolved by name at mount time. A missing registration is a runtime miss rendered visibly, not a compile error.
- The store is the only state. Paths are
$/scope/...; scopes are lifetimes, not folders. A component that copies store state intouseStatehas created a second answer to one question. useStoreis state,useStoreValueis a view.useStore(path, initial)seeds the path when nothing has filled it in, the wayuseStatedoes, so every reader agrees.useStoreValue(path, fallback)never writes: its fallback is what that one component displays while the path is empty.- A component fills the space it is given; it does not ask for space. A viewer, list, tree or table with
flexor aheightrenders what fits and scrolls -useMeasuresays how much that is. Sizing from content is what makes a pane move when a different file is opened. - Colour is inherited; a tone comes with what to write on it. A
texttakes its parent'sfg/bgunless it states its own, so a fixedfg="muted"inside a row that can be selected is a bug. UseTONE/ON_TONErather than pairing a tone withinvertedby hand. - Selection inverts. A control at rest is a line and a label in its tone; selected, the tone becomes the background. Recolouring only a border is not findable, and loses to any filled control beside it.
- Meaning never depends on colour alone. A status is a glyph and a colour, because a 16-colour session, a colourblind reader and a piped log all lose the colour.
- Glyphs come from the theme. Hardcoding
'│'or'●'in a component is how an ascii terminal ends up with a row of question marks. - Sizes are whole cells. Fractions are distributed by largest remainder so three flex-1 children of a 10-cell row get 4, 3, 3 - never 3, 3, 3.
playground/test/playgrounds.test.tsx mounts every playground, resizes it, strips the terminal's capabilities and asserts nothing broke. Adding a playground means adding it to playground/src/registry.ts; nothing else picks it up, and nothing else checks it.