Repository navigation
Releases
Releases · BaselAshraf81/layout-sans
LayoutSans now has real text interaction: selection, copy, search & links on canvas (pure TS, no DOM layout)
LayoutSans now has real text interaction: selection, copy, search & links on canvas (pure TS, no DOM layout)
Latest
v0.2 — Interactive Text Engine
This release introduces a robust, canvas-based interaction layer, transforming the engine from a static layout tool into a fully interactive text environment.
Core Interaction Features
- Canvas-Based Selection: Support for native-feeling text selection including click-drag, double-click for word selection, sub-glyph precision, and shift-click to extend ranges.
- Programmatic Control: Full API access to manage selection states and cursors.
- System Integration: Native OS clipboard integration supporting standard shortcuts (e.g.,
Ctrl+C). - Search & Navigation: Integrated
Ctrl+Ffull-text search featuring highlighted matches and jump-to-navigation.
Accessibility & Navigation
-
Shadow Semantic Tree: A virtualized accessibility layer using
$O(\text{viewport})$ DOM nodes to ensure compatibility with screen readers like VoiceOver, NVDA, and JAWS. - Focus Management: Full keyboard focus support coupled with custom canvas-drawn focus rings.
- Interactive Elements: Support for clickable hyperlinks including reactive hover states.
Performance & Architecture
- Spatial Hit-Testing: Utilizes a packed R-Tree for spatial queries, maintaining high performance even with 100k+ interactive items.
- Layout Core: Built upon the high-performance pure-math layout engine powered by Pretext for sub-pixel accurate text measurement.
Live Demo
Experience 100k variable-height items with full text interactivity:
https://baselashraf81.github.io/layout-sans/demo/interactive-text.html
v0.1.2 — Bug fixes and performance improvements
🐛 Bug Fixes
- Fixed absolute container positioning — Absolute containers now correctly report their resolved x/y position instead of inheriting from the first child
- Fixed crash at 65k+ nodes — Replaced spread operator patterns that hit V8's call stack limit, enabling layouts with 100k+ nodes
- Fixed flex-shrink calculation — Now uses
flexBasisinstead ofmainSizeper CSS Flexbox spec - Fixed magazine column overflow — Content blocks can now flow across unlimited columns instead of being truncated after 2
⚡ Performance
- Optimized measureNode — Added lightweight
measureFlexSize()andmeasureGridSize()paths that avoid allocating full layout records, eliminating O(n²) work for nested flex containers - Added flex row fast-path — Horizontal fixed-size lists now use an O(n) fast path, matching the existing column optimization
📊 Benchmarks
| Scenario | LayoutSans | vs DOM | vs Yoga WASM |
|---|---|---|---|
| 100 flex boxes | 0.25ms | 32× faster | 3× faster |
| 10,000 flex boxes | 8.55ms | 94× faster | 1× (comparable) |
| 100,000 var-height items | 50.01ms | ∞× (DOM crashes) | 2× faster |
What's Changed
Full changelog: https://github.com/BaselAshraf81/layout-sans/blob/main/CHANGELOG.md
Install
npm install layout-sans@0.1.2v0.1.0 – Pretext-powered 2D Layout Engine (Pure TS, No DOM, No WASM)
🎉 LayoutSans v0.1.0 is here!
The missing layer after Pretext.
Pretext gives you perfect text measurement without DOM reflows.
LayoutSans takes those measurements and gives you full 2D layout: Flex, basic Grid, Magazine-style multi-column, Absolute positioning — all in pure TypeScript.
- Zero DOM — never touches
getBoundingClientRector the browser layout engine - Zero WASM bloat — ~3.7 kB gzipped (core)
- Universal — runs in Browser, Node, Bun, Deno, Cloudflare Workers, Edge
- Pretext integration — accurate text measurement via peer dependency
- Virtualization-ready — flat output perfect for 100k+ variable-height items at 120 fps
Highlights
- Full Flexbox support (row/column, gap, justify, align, wrap, flex-grow/shrink)
- Basic Grid & Magazine multi-column text flow
- Absolute positioning
- Text nodes powered by
@chenglou/pretext - Clean 5-line API with
.compute()returning positioned boxes - Benchmarks included (10k–100k boxes)
5-line demo
import { createLayout } from 'layout-sans'
const engine = createLayout({
type: 'flex',
direction: 'row',
width: 800,
gap: 16,
children: [
{ type: 'box', flex: 1 },
{ type: 'box', width: 240 }
]
})
const boxes = engine.compute()