Releases: bikeshaving/termdom
Release list
TermDOM 0.1.5
0.1.5
Added
- CSS Grid: track sizing, placement and alignment in the layout engine; grid shorthand parsing and serialization; used track sizes resolved through the CSSOM.
- Element scrolling: overflow containers clip,
scrollTop/scrollLeftclamp against content extents and repaint, wheel events chain through nested scrollers, andscrollWidth/scrollHeightread off the layout tree. - The clipboard:
ClipboardEventandDataTransfer, apasteevent with clipboard items and permissions, transfers emptied when their dispatch ends, and the clipboard gated on user activation. Selection.modify(), with character, word and line granularities.- Sequential focus navigation follows HTML's scope model. The document, each shadow root and each slot form their own focus scope; a scope sorts by tabindex within itself and sits at its owner's position in the parent scope.
delegatesFocus, slot fallback content, and negative-tabindex scopes (unreachable by Tab, navigable once focus is scripted inside) behave as specified. Tab past the last focusable rests on nothing, as past a browser's page. user-select: none— the mouse cannot anchor or extend a selection into it,Selection.modifywalks past it, and the painter lays no highlight over it.user-select: textre-enables selection inside anoneancestor.aspect-ratio, with the ratio counted in cells.- The CSS system colors, mapped onto the terminal palette.
<details>gets a user-agent shadow tree: a closed details hides its body, bare text children included.- Number inputs edit like numbers: explicit grammar, input filtering, arrow stepping, and the
valueAsNumberfamily. Node.moveBefore— the atomic move primitive, with its validations, live-range and iterator bookkeeping, slot reassignment, andconnectedMoveCallback(falling back to the disconnected/connected pair).- New event interfaces:
MessageEvent,HashChangeEvent,StorageEvent,DragEvent,TextEvent, and acreateEventtable covering the spec's list. The sensor and touch names throw, because they name hardware a terminal does not have. - Geometry and hit-testing surface:
document.elementsFromPoint,checkVisibility,Range.createContextualFragment,Document.getElementsByName, and MouseEvent's page, offset and movement coordinate spaces. - IME composition: the terminal passes through the keys a composition is built from, shows the syllable while it is being composed, and delivers it whole.
Changed
<kbd>renders bold and underlined, the way TUIs have marked accelerators, replacing the bracketed rendering.:focus,:focus-withinand:host(:focus)are shadow-aware. The focused element and the shadow hosts above it match:focus;:focus-withinclimbs the ancestor-and-host chain; a focus change restyles that chain.- The width tables are generated from the Unicode Character Database.
event.isTrustedis honest: true for events the engine dispatched from real input, false for an author's.initialin author styles resolves to the spec's initial value rather than the element default.- Documents without an engine behave per spec. A document from
createHTMLDocumentorDOMParseranswers with zero geometry, state-only focus and rejecting fullscreen instead of throwing.
Fixed
- A widget-heavy initial render dropped from 43 seconds to 374 milliseconds: a form widget's shadow
<style>was rebuilding the document's whole cascade several times over. A shadow sheet now refreshes its own root, and initial render is linear in document size again. focus()works inside shadow trees,document.activeElementretargets to the host chain, andShadowRoot.activeElementanswers with its own descendant.- Focus in fullscreen: size reveals by the screen, and blur runs before Escape exits.
requestAnimationFramechains tick: a callback scheduling the next frame runs on that next frame, and awaiting a frame still means the frame that includes your pending mutations has landed.- A pseudo-element's used box metrics resolve from its own layout box.
grid-template-areasserializes as its cells, a single space apart.- An app that closes right after a frame no longer leaks cursor-position reports into the shell that inherits the terminal.
- A document closing while fullscreen leaves no trace, the way an alt-screen program does: the screen restores to what stood before entry, with the shell continuing below it. Exiting fullscreen before closing still pays the final flow state out to scrollback.
Conformance
The WPT DOM suites run against the engine itself: each test file mounts as the initial document of a real TermDOM over a mock transport, with a testdriver backend that turns WebDriver key and click actions into the bytes a terminal sends. 95,064 subtests pass and 1,389 fail, with each excluded file naming its reason — see docs/dom-conformance.md and docs/cssom-conformance.md.
Release notes drafted by Claude (Fable 5).
TermDOM 0.1.4
Rendering and layout:
- Fire the
resizeevent onwindow. - Fix borders not joining for some parent/child layouts.
- Add rounded corners when
border-radiusis set. - Remove whitespace-only text between flex items regardless of what the neighboring items display as (css-flexbox-1 §4).
- Stop collapsing spaces under
white-space: break-spaces.
DOM and CSSOM:
- Fix serialization, the
dirattribute, and event dispatch, including retargeting across shadow trees. - Write user edits to a field's value slot instead of calling its value setter (this broke React)
- Track live ranges and node iterators per tree and release them with it.
- Define indexed access on collections and computed styles as accessor properties.
- Invalidate styles on every attribute writer (
classList,className,toggleAttribute, the parser) and register declaratively parsed shadow roots with the cascade.
Width measurement:
- Regenerate the character width tables from the Unicode Character Database at 17.0.0. Trigrams, hexagrams, counting rods, and recent emoji now measure two cells.
- Measure clusters that only appear against the right margin, where a reply is ambiguous, at the left edge of a row the frame repaints anyway.
Examples:
- Add hello-world examples for React, Vue, Svelte, and Crank, each running under Node with no build step.
Internal:
- Adopt @b9g/eslint-config across the codebase; remove WeakRef, most proxies, and the compile-cache call from the entry module.
TermDOM 0.1.3
Two patches.
- The mode probes sent at attach (
ESC[8$p,ESC[?2027$p) left a straypvisible on terminals without DECRQM, Terminal.app included. An erase after the probes cleans up whatever the terminal echoed. (#41) @layerand@scoperules now participate in the cascade — previously they parsed into the CSSOM and styled nothing. Full css-cascade-5 layer ordering (including the !important reversal),@scopewith proximity, selector specificity computed from the selector AST per selectors-4 (:is()/:not()take their heaviest argument,:where()weighs zero), and theflex-flow/place-content/place-items/place-selfshorthands expand.word-breakandoverflow-wrapwere always implemented; the compatibility probes now show it. 170 CSS properties supported. (#42)
Full suite: node 1247, bun 1272.
TermDOM 0.1.2
63 commits since 0.1.1.
New
dialog.showModal(): the top layer,::backdrop,:modal, focus containment, and Escape firingcancel. Modal dialogs paint over the page and block interaction behind them. (#32)- The Popover API:
popoverattribute states,showPopover()/hidePopover()/togglePopover(),popovertargetinvokers,beforetoggle/toggleevents, light dismiss, stacking, and:popover-open. (#38) border-radius: the shorthand and all four longhands, including slash syntax. A corner with a nonzero radius renders with the rounded box-drawing glyph. (#31)- CSS logical properties: the
margin/padding/inset/borderblock and inline families,block-size/inline-sizewith min/max variants, andtext-align: start/end, resolved againstdirectionby cascade order. (#33) beforeunload:window.close()and Ctrl-C dispatch a cancelableBeforeUnloadEvent, so an app can ask before quitting. (#29)<kbd>renders as a bold bracketed keycap. (#28)- Terminal width measurement: where a terminal doesn't negotiate mode 2027, the engine measures ambiguous glyph advances (VS16 emoji, East Asian Ambiguous, Arabic presentation forms) with in-frame cursor queries and corrects itself, fixing emoji misalignment in Terminal.app. (#37)
window.scrollXandpageXOffsetread 0.
Fixed
- Preserved spaces no longer collapse at inline element boundaries under
white-space: pre. (#36) - Attribute values containing selector metacharacters parse correctly — thanks @yowainwright for the first outside contribution. (#39)
text-align: endmaps to the left edge underdirection: rtl.- Absolutely positioned boxes with both insets and
margin: autocenter per CSS 2.1 instead of stretching. - Enter activates checkboxes, as Space does.
- The engine no longer uses
Buffer; browser bundles need no polyfill, andProcessLikeacceptsUint8Array/ArrayBufferchunks.
Examples
weather.ts: an Open-Meteo forecast with hourly charts drawn in eighth-blocks.popover.ts: a menu bar where every menu is a declarative popover.
Docs
- The README explains how the engine works;
COMPATIBILITY.mdclassifies every CSS property as measured, not applicable to a character grid, or not yet implemented; the conformance docs are regenerated (94,816 DOM subtests passing, 3,124 CSSOM).
TermDOM 0.1.1
TermDOM now ships its own DOM and CSSOM
jsdom and cssstyle are gone. The DOM tree, mutation algorithms, events with shadow retargeting, live collections, ranges and selection, custom elements, and the full CSSOM — stylesheets, rules, declarations, computed and resolved values — are TermDOM's own, written against the specs with web-platform-tests as the referee (94,800+ DOM subtests passing at the swap, more fixed since). Parsing is parse5 and css-tree; selectors are nwsapi. Author-visible APIs follow one rule: spec-complete or absent. getComputedStyle resolved values now read real layout, styleEl.sheet returns the element's actual stylesheet, and ResizeObserver honors options.box.
Rendering changes to know about
- Margin collapsing is now fully per CSS 2.2 §8.3.1 — including self-collapsing empty blocks and negative margins, which previously didn't parse. If a layout leaned on the old behavior, vertical gaps can shift. One knowing deviation: the body's edges stop the collapse.
- Block containers are laid out by a real block algorithm instead of a flex emulation:
gapandorderno longer misapply to blocks, a wrapping flex container keeps its later lines inside its own box, andinline-flexestablishes a formatting context.
Fixes
Around thirty rendering bugs found by a new differential fuzzer and property suite, among them: hidden (display: none) subtrees kept reporting geometry; content vanishing or misordering around blocks inside inlines and display: contents elements; nested inline-block content never laid out after edits; whitespace-only blocks under white-space: pre collapsing to nothing; hit-testing missing ::before content and dissolved boxes; a stray <svg> crashing style resolution; counter(x, lower-alpha) wrong past 26; vw/vh values stale across terminal resizes; multi-line pastes into a <textarea> arriving as one line (terminals send pasted newlines as CR); mouse coordinates disagreeing with painted cells in fixed and scrolled content.
Performance
Invalidation is targeted instead of global: a class flip on one row of a long list re-renders that row, not the document. Live-collection updates during bulk insertion went from quadratic to linear (a 5,000-item filter list: 1,243ms → 162ms per keystroke), and a heap leak that pinned removed nodes for the life of the process is fixed.
Examples and docs
examples/solitaire.ts is new and leads the README: full-keyboard Klondike (cursor, Tab, Enter), one- and three-card draw from a <dialog> menu, responsive card sizes via @media, fullscreen, a speedrun clock with per-deal bests — deals are seeded, and node examples/solitaire.ts 13 replays the demo GIF's game. The getting-started guide gains verified framework recipes: React 19, Vue 3.5, Svelte 5, and Crank each mount with a few lines of globals.
For contributors
npm test now carries a differential fuzzer regression net (incremental rendering must equal fresh rendering, byte for byte, over 31 historical seeds) and six fast-check properties covering rendering, computed styles, geometry/hit-testing agreement, resize round-trips, serialization fixpoints, and field editing against a reference model.
TermDOM 0.1.0
The first release.
TermDOM is a JavaScript library that displays HTML and CSS in the terminal. It draws actual DOM nodes to terminal output and redraws the screen when they mutate, so TUIs and interactive CLIs can be written with vanilla JavaScript or any frontend web framework.
npm install @b9g/termdomimport {TermDOM} from "@b9g/termdom";
const term = new TermDOM();
term.attach();
term.document.body.innerHTML = `<div style="color: green; border: 1px solid">hello</div>`;What's in it:
- A real DOM document with a CSS cascade and a cell-based layout engine: box model, flexbox, tables, and inline layout. One cell is
1chwide and1pxtall. - Stylesheets from
<style>elements andstyleattributes, translated to ANSI color and text decoration. - Keyboard, mouse, focus, and paste events fired on elements, the document, and the window.
<input>,<textarea>,<select>, checkboxes, and radios as shadow-tree widgets with terminal-native looks, restylable with CSS. The caret is the real terminal cursor, so IMEs compose in the field.- CJK, emoji, and combining characters at their correct widths; Hebrew and Arabic in visual order with contextual shaping.
- Scrolling with
window.scrollTo()andelement.scrollIntoView(); the alternate screen viaelement.requestFullscreen(). - Web Components:
customElements.define(),attachShadow(),<slot>,:host. - Drag selection styled with
::selection;navigator.clipboardover OSC 52.
Documentation: https://termdom.org
Guides: https://termdom.org/guides/getting-started/
Compatibility matrix: https://termdom.org/compatibility/
Examples: https://github.com/bikeshaving/termdom/tree/main/examples