diff --git a/README.md b/README.md
index a59b67f6..29682e6f 100644
--- a/README.md
+++ b/README.md
@@ -1,296 +1,201 @@
-[](https://github.com/dhilt/vscroll/actions/workflows/build.yml)
+[](https://github.com/dhilt/vscroll/actions/workflows/general.yml)
[](https://www.npmjs.com/package/vscroll)
# VScroll
+A framework-independent virtual scrolling engine for JavaScript and TypeScript.
+
- [Overview](#overview)
-- [Getting started](#getting-started)
+- [Installation](#installation)
- [Usage](#usage)
- - [Consumer](#1-consumer)
- - [Element](#2-element)
- - [Datasource](#3-datasource)
- - [Run](#4-run)
- - [Routines](#5-routines)
-- [Live](#live)
- [Adapter API](#adapter-api)
+- [Documentation](#documentation)
- [Thanks](#thanks)
## Overview
-VScroll is a JavaScript library providing virtual scroll engine. Can be seen as a core for platform-specific solutions designed to represent unlimited datasets using virtualization technique. Below is the diagram of how the VScroll engine is being distributed to the end user.
+Virtual scrolling is a technique for displaying large lists efficiently. Instead of rendering every item at once, it keeps a small set of items in the DOM — those in and around the visible area — and updates that set as the user scrolls. This reduces DOM size and rendering work while preserving a familiar scrolling experience.
+
+VScroll provides a framework-independent **core engine** for virtual scrolling. An application can use it directly or through a platform-specific wrapper called a **consumer**. The diagram shows how the engine reaches the end user when a consumer is used.
-
-
+
-Basically, the consumer layer can be omitted and the end Application developers can use VScroll directly. This repository has a [minimal demo page](https://dhilt.github.io/vscroll/) of direct use of the VScroll library in a non-specific environment. There are also several consumer implementations built on top of VScroll:
+The [minimal browser demo](https://dhilt.github.io/vscroll/) demonstrates direct use of VScroll without a separate consumer.
+
+Existing consumers and integration examples include:
- - [ngx-ui-scroll](https://github.com/dhilt/ngx-ui-scroll), Angular virtual scroll directive
- - [vscroll-native](https://github.com/dhilt/vscroll-native), virtual scroll module for native JavaScript applications
- - [Vue integration sample](https://stackblitz.com/edit/vscroll-vue-integration?file=src%2Fcomponents%2FVScroll.vue), very rough implementation for Vue
+- [ngx-ui-scroll](https://github.com/dhilt/ngx-ui-scroll) — an Angular virtual scrolling directive.
+- [vscroll-native](https://github.com/dhilt/vscroll-native) — a virtual scrolling module for native JavaScript applications.
+- [Vue integration sample](https://stackblitz.com/edit/vscroll-vue-integration?file=src%2Fcomponents%2FVScroll.vue) — an example of using VScroll in Vue.
-## Getting started
+## Installation
### CDN
+Load the library in a browser and access its exports through `VScroll`:
+
```html
```
+For reproducible deployments, pin the CDN URL to a package version.
+
### NPM
-```
+```sh
npm install vscroll
```
-```js
-import { Workflow } from 'vscroll';
-
-const workflow = new Workflow(...);
-```
-
-## Usage
-
-The main entity distributed via `vscroll` is the `Workflow` class. Its instantiating runs the virtual scroll engine.
+Import the library in the application's build:
```js
-new Workflow({ consumer, element, datasource, run });
-```
-
-The constructor of the `Workflow` class requires an argument of the following type:
-
-```typescript
-interface WorkflowParams {
- consumer: IPackage;
- element: HTMLElement;
- datasource: IDatasource;
- run: OnDataChanged;
- Routines?: RoutinesClassType;
-}
-```
-
-This is a TypeScript definition, but speaking of JavaScript, an argument object must contain 4 mandatory and 1 optional fields described below.
-
-### 1. Consumer
-
-A simple data object that provides information about a consumer. It is not critical to omit this, but if the result solution is going to be published as a separate 3d-party library ("consumer"), the name and the version of the result package should be passed as follows:
+import * as VScroll from 'vscroll';
-```js
-const consumer = {
- name: 'my-vscroll-consumer',
- version: 'v1.0.0-alpha.1'
-};
-```
-
-### 2. Element
-
-An HTML element the `Workflow` should use as a scrollable part of the viewport. It should be present in DOM before instantiating the `Workflow`.
-
-```js
-const element = document.getElementById('vscroll');
-```
-
-This element should be wrapped with another container with constrained height and overflow scroll/auto. And it also must have two special padding elements marked with special attributes for the virtualization purpose.
-
-```html
-
-```
-
-```css
-#viewport {
- height: 300px;
- overflow-y: scroll;
-}
-```
-
-### 3. Datasource
-
-This is a special object, providing dataset items in runtime. There is a separate wiki document describing the Datasource: [github.com/dhilt/vscroll/wiki/Datasource](https://github.com/dhilt/vscroll/wiki/Datasource). Below is a short version.
-
-The Datasource can be defined in two ways. First, as an object literal:
-
-```js
-const datasource = {
- get: (index, count, success) => {
- const data = [];
- for (let i = index; i < index + count; i++) {
- data.push({ id: i, text: 'item #' + i });
- }
- success(data);
- }
-};
+new VScroll.Workflow(...);
```
-Second, as an instance of Datasource class which can be obtained through a special factory method. Along with the `Workflow` class, VScroll exposes the `makeDatasource` method which can be used for creating Datasource class, so the end datasource object can be instantiated via operator `new`:
-
-```js
-import { makeDatasource } from 'vscroll';
-const Datasource = makeDatasource();
-
-const datasource = new Datasource({
- get: (index, length, success) =>
- success(Array.from({ length }).map((_, i) =>
- ({ id: index + i, text: 'item #' + (index + i) })
- ))
-});
-```
-
-The argument of the Datasource class is the same object literal as in the first case. It has one mandatory field which is the core of the App-Scroller integration: method `get`. The `Workflow` requests data via the `Datasource.get` method in runtime.
-
-For more solid understanding the concept of the Datasource with examples, please, refer to [the Datasource doc](https://github.com/dhilt/vscroll/wiki/Datasource).
-
-### 4. Run
-
-A callback that is called every time the Workflow decides that the UI needs to be changed. Its argument is a list of items to be present in the UI. This is a consumer responsibility to detect changes and display them in the UI.
-
-```js
-const run = newItems => {
- // assume oldItems contains a list of items that are currently present in the UI
- if (!newItems.length && !oldItems.length) {
- return;
- }
- // make newItems to be present in the UI instead of oldItems
- processItems(newItems, oldItems);
- oldItems = newItems;
-};
-```
-
-Each item (in both `newItems` and `oldItems` lists) is an instance of the [Item class](https://github.com/dhilt/vscroll/blob/v1.5.0/src/classes/item.ts) implementing the [Item interface](https://github.com/dhilt/vscroll/blob/v1.5.0/src/interfaces/item.ts), whose props can be used for proper implementation of the `run` callback:
-
-|Name|Type|Description|
-|:--|:--|:----|
-|element|_HTMLElement_|HTML element associated with the item|
-|$index|_number_|Integer index of the item in the Datasource. Correlates with the first argument of the Datasource.get method|
-|data|_Data_|Data (contents) of the item. This is what the Datasource.get passes to the Scroller via success-callback as an array of data-items typed as Data[]|
-|invisible|_boolean_|Flag that determines whether the item should be hidden (if _true_) or visible (if _false_) when the _run_ method is called|
-|get|_() => ItemAdapter<Data>_|Shortcut method returning { element, $index, data } object|
-
-`Run` callback is the most complex and environment-specific part of the `vscroll` API, which is fully depends on the environment for which the consumer is being created. Framework specific consumer should rely on internal mechanism of the framework to provide runtime DOM modifications.
-
-There are some requirements on how the items should be processed by `run` call.
+## Usage
-- After the `run` callback is completed, there must be `newItems.length` elements in the DOM between backward and forward padding elements.
-- Old items that are not in the new items list should be removed from DOM. Use `oldItems[].element` references for this purpose.
-- Old items that are in the new items list should not be removed and recreated, as this may result in unwanted scroll position shifts. Just don't touch them.
-- New items elements should be rendered in the correct order. Specifically, in accordance with `newItems[].$index` comparable to `$index` of elements that remain: `$index` must increase continuously and the directions of increase must persist across the `run` calls. The scroller maintains `$index` internally, so you only need to properly inject a set of `newItems[].element` into the DOM.
-- New elements should be rendered without being visible, and this should be achieved by "fixed" positioning and "left"/"top" coordinates that take the item element out of view. The Workflow will take care of visibility after calculations. An additional `newItems[].invisible` attribute can be used to determine whether a given element should be hidden. This requirement can be changed by the `Routines` class setting (see below).
-- New items elements should have a "data-sid" attribute whose value should reflect `newItems[].$index`.
+A `vscroll` consumer is responsible for the integration: supplying data when requested by the engine and rendering the current buffer in the DOM. The engine manages scrolling, determines which items are needed, and updates the buffer; the consumer defines how data is retrieved and displayed. This integration is configured when creating `Workflow`, the main entry point to the engine.
-### 5. Routines
+### Workflow
-A special class allowing to override the default behavior related to the DOM. All DOM-specific operations are implemented as the [DOM Routines class](https://github.com/dhilt/vscroll/blob/v1.5.0/src/classes/domRoutines.ts) methods inside core. When the `Routines` class setting is passed among the Workflow arguments, it replaces the core Routines. The custom Routines class must extend the core class, which can be taken from the VScroll imports:
+The Workflow class, exported by vscroll, is where the integration is configured. Instantiating it starts the engine. See the [Workflow reference](docs/workflow.md) for the requirements behind its constructor parameters.
```js
-import { Routines, Workflow } from 'vscroll';
-
-class CustomRoutines extends Routines { ... }
-
-new Workflow({
- consumer, element, datasource, run, // required params
- Routines: CustomRoutines
-})
-```
-
-The Routines methods description can be taken from the [IRoutines interface](https://github.com/dhilt/vscroll/blob/v1.5.0/src/interfaces/routines.ts) sources. For example, there is a method that calculates the scroller's offset:
-
-```typescript
-getOffset(): number {
- const get = (element: HTMLElement) =>
- (this.settings.horizontal ? element.offsetLeft : element.offsetTop) || 0;
- return get(this.element) - (!this.settings.window ? get(this.viewport) : 0);
-}
+const workflow = new VScroll.Workflow({ consumer, element, datasource, run, Routines });
```
-If we have a table layout case where we need to specify the offset of the table header, the base method can be overridden as follows:
+| Parameter | Purpose |
+| --- | --- |
+| `consumer` | Static integration metadata (`name` and `version`), used in diagnostics. |
+| `element` | The mounted DOM element containing the rendered list, not the scrollable viewport. |
+| `datasource` | The object that supplies data and scrolling settings, described below. See also [Datasource](docs/datasource.md). |
+| `run(items)` | The callback that keeps the rendered list in sync with the complete current buffer, including offscreen items. See [Rendering](docs/rendering.md). |
+| `Routines` | Optional subclass of `VScroll.Routines` for customizing DOM operations and render scheduling. See [Custom Routines](docs/routines.md). |
-```js
-new Workflow({
- consumer, element, datasource, run, // required params
- Routines: class extends Routines {
- getOffset() {
- return document.querySelector('#viewport thead')?.offsetHeight || 0;
- }
- }
-});
-```
+### Datasource
-It's worth noting that thanks to the extending, we can use parent methods and have access to the correct context after the engine instantiates the Routines:
+Every `Workflow` requires a datasource object to supply items on request and, optionally, configure scrolling. Its data and configuration fields are `{ get, settings, devSettings }`. See the [Datasource reference](docs/datasource.md) for the full contract, supported signatures, and implementation examples.
-```js
-class CustomRoutines extends Routines {
- onInit(...args) {
- console.log('Routines settings:', this.settings);
- super.onInit(...args);
- }
-}
-```
+- **`get`** is the required data retrieval function, called with a starting index and item count. It can be synchronous or asynchronous. A minimal callback example providing a synchronous, infinite data stream:
-Various DOM calculations, setting/getting the scroll position, render process and other logic can be adjusted, improved or completely replaced by custom methods of the `Routines` class setting.
+ ```js
+ const get = (index, count, callback) =>
+ callback(Array.from({ length: count }, (_, i) => `Item ${index + i}`));
+ ```
-## Live
+- **`settings`** is an optional object for configuring scrolling. The table below summarizes its options and defaults. See [Configuration](docs/configuration.md#settings) for types, constraints and examples.
-This repository has a minimal demonstration of the App-consumer implementation considering all of the requirements listed above: https://dhilt.github.io/vscroll/. This is all-in-one HTML demo with `vscroll` taken from CDN. The source code of the demo is [here](https://github.com/dhilt/vscroll/blob/main/demo/index.html). The approach is rough and non-optimized, if you are seeking for more general solution for native JavaScript applications, please have a look at [vscroll-native](https://github.com/dhilt/vscroll-native) project. It is relatively new and has no good documentation, but its [source code](https://github.com/dhilt/vscroll-native/tree/main/src) and its [demo](https://github.com/dhilt/vscroll-native/tree/main/demo) may shed light on `vscroll` usage in no-framework environment.
+ | Setting | Default | Purpose |
+ | --- | --- | --- |
+ | [`startIndex`](docs/configuration.md#bounds-and-initial-positioning) | `1` | Initial item index, clamped to the configured bounds. |
+ | [`minIndex`](docs/configuration.md#bounds-and-initial-positioning) | `-Infinity` | Inclusive lower dataset index bound. |
+ | [`maxIndex`](docs/configuration.md#bounds-and-initial-positioning) | `Infinity` | Inclusive upper dataset index bound. |
+ | [`padding`](docs/configuration.md#settings) | `0.5` | Extra buffered area on each side, in viewport sizes. |
+ | [`bufferSize`](docs/configuration.md#settings) | `5` | Minimum fetch batch target, not a limit on buffered items. |
+ | [`itemSize`](docs/configuration.md#size-estimates-and-layout) | `NaN` | Initial item-size estimate in pixels; measured automatically when omitted. |
+ | [`sizeStrategy`](docs/configuration.md#size-estimates-and-layout) | `'average'` | Estimate unknown item sizes using `'average'`, `'frequent'` or `'constant'`. |
+ | [`viewportElement`](docs/configuration.md#viewport-and-horizontal-scrolling) | `null` | Custom viewport element or element factory; defaults to the content element's parent. Experimental. |
+ | [`windowViewport`](docs/configuration.md#viewport-and-horizontal-scrolling) | `false` | Use the browser window as the viewport. |
+ | [`horizontal`](docs/configuration.md#viewport-and-horizontal-scrolling) | `false` | Scroll horizontally instead of vertically. |
+ | [`inverse`](docs/configuration.md#other-settings) | `false` | Align short content to the bottom or right without reversing item order. Experimental. |
+ | [`infinite`](docs/configuration.md#settings) | `false` | Keep loaded items instead of clipping them automatically. |
+ | [`onBeforeClip`](docs/configuration.md#settings) | `null` | Receive clipped items just before they leave the buffer. Experimental. |
-Another example is [ngx-ui-scroll](https://github.com/dhilt/ngx-ui-scroll). Before 2021 `vscroll` was part of `ngx-ui-scroll`, and its [demo page](https://dhilt.github.io/ngx-ui-scroll/#/) contains well-documented samples that can be used to get an idea on the API and functionality offered by `vscroll`. The code of the [UiScrollComponent](https://github.com/dhilt/ngx-ui-scroll/blob/v2.3.1/src/ui-scroll.component.ts) clearly demonstrates the `Workflow` instantiation in the context of Angular. Also, since ngx-ui-scroll is the intermediate layer between `vscroll` and the end Application, the Datasource is being provided from the outside. Method `makeDatasource` is used to provide `Datasource` class to the end Application.
+- **`devSettings`** is an optional object for logging, timing, caching and scroll behavior. See [Development settings](docs/configuration.md#development-settings) for its options and defaults.
## Adapter API
-Adapter API is a powerful feature of the `vscroll` engine allowing to collect the statistics and provide runtime manipulations with the viewport: adding, removing, updating items. This API is very useful when building the real-time interactive applications when data can change over time by not only scrolling (like chats).
-
-Please refer to the ngx-ui-scroll [Adapter API doc](https://github.com/dhilt/ngx-ui-scroll#adapter-api) as it can be applied to `vscroll` case with only one important difference: vscroll does not have RxJs entities, it has [Reactive](https://github.com/dhilt/vscroll/blob/main/src/classes/reactive.ts) ones instead. It means, for example, `eof$` has no "subscribe" method, but "on":
-
-```js
-// ngx-ui-scroll
-myDatasource.adapter.bof$.subscribe(value =>
- value && console.log('Begin of file is reached')
-);
-// vscroll
-myDatasource.adapter.bof$.on(value =>
- value && console.log('Begin of file is reached')
-);
-```
-
-Adapter API becomes available as the `Datasource.adapter` property after the Datasource is instantiated via operator "new". In terms of "vscroll" you need to get a Datasource class by calling the `makeDatasource` method, then you can instantiate it. `makeDatasource` accepts 1 argument, which is an Adapter custom configuration. Currently this config can only be used to redefine the just mentioned Adapter reactive props. Here's an example of how simple Reactive props can be overridden with RxJs Subject and BehaviorSubject entities: [ui-scroll.datasource.ts](https://github.com/dhilt/ngx-ui-scroll/blob/v2.3.1/src/ui-scroll.datasource.ts).
-
-An important note is that the Adapter getting ready breaks onto 2 parts: instantiation (which is synchronous with the Datasource instantiation) and initialization (which occurs during the Workflow instantiating). Adapter gets all necessary props and methods during the first phase, but they start work only when the second phase is done. Practically this means
- - you may arrange any Adapter reactive subscriptions in your app/consumer right after the Datasource is instantiated,
- - some of the initial (default) values can be unusable, like `Adapter.bufferInfo.minIndex` = NaN (because Scroller's Buffer is empty before the very first `Datasource.get` call),
- - Adapter methods do nothing when called before phase 2, they immediately resolve some default "good" value (`{ immediate: true, success: true, ... }`).
+The Adapter API extends the scrolling engine with reactive state observation and runtime control. It provides access to loading state, visible items and dataset boundaries, supports adding, removing and updating items or reloading data, and enables synchronization of application actions with scroller activity. These capabilities support interactive interfaces such as chats, live feeds and editable lists, where content evolves in response to incoming data and user actions.
-If there is some logic that could potentially run before the Adapter initialization and you don't want this to happen, the following approach can be applied:
+The Adapter API is available when a datasource is created through the `makeDatasource` factory exported by `vscroll`.
```js
-myDatasource = new VScroll.makeDatasource()({...});
-myDatasource.adapter.init$.once(() => {
- console.log('The Adapter is initialized'); // 2nd output
-});
-workflow = new VScroll.Workflow({...});
-console.log('The Workflow runs'); // 1st output
-```
-
-VScroll will receive its own Adapter API documentation later, but for now please refer to [ngx-ui-scroll](https://github.com/dhilt/ngx-ui-scroll#adapter-api).
+const Datasource = VScroll.makeDatasource();
+const datasource = new Datasource({ get, settings });
+const adapter = datasource.adapter;
+
+// Reload data when the refresh button is clicked.
+refreshButton.addEventListener('click', () => adapter.reload());
+
+// Log loading state changes.
+adapter.isLoading$.on(isLoading => console.log('Loading:', isLoading));
+```
+
+The Adapter is created when the datasource is instantiated. Its reactive properties can be observed before constructing `Workflow`, but method calls have no effect until `Workflow` finishes initializing. See [Calling Adapter methods](docs/adapter-methods.md#calling-methods).
+
+`makeDatasource` also accepts an optional configuration factory for customizing the Adapter's reactive properties. See [Custom Adapter reactivity](docs/datasource.md#consumer-specific-adapter-reactivity).
+
+The tables below provide a brief overview of the Adapter's properties and methods. See [Adapter properties](docs/adapter.md) and [Adapter methods](docs/adapter-methods.md) for details, and the [ngx-ui-scroll Adapter demos](https://dhilt.github.io/ngx-ui-scroll/#adapter) for interactive examples.
+
+### Properties
+
+Properties are read-only. Each `$` counterpart provides [reactive updates](docs/adapter.md#reactive-subscriptions).
+
+| Property | Purpose |
+| --- | --- |
+| `init`, `init$` | Whether the Adapter is initialized. |
+| `isLoading`, `isLoading$` | Whether a workflow cycle is running, including fetching and rendering. |
+| `loopPending`, `loopPending$` | Whether an inner workflow loop is running. |
+| `paused`, `paused$` | Whether workflow processing is paused. |
+| `bufferInfo` | Buffer, cache and dataset index bounds, plus the estimated item size. |
+| `itemsCount` | Number of rendered buffer items, including offscreen items. |
+| `firstVisible`, `firstVisible$` | First item intersecting the viewport, including a partially visible item. |
+| `lastVisible`, `lastVisible$` | Last item intersecting the viewport, including a partially visible item. |
+| `bof`, `bof$` | Whether the buffer has reached the dataset's beginning. |
+| `eof`, `eof$` | Whether the buffer has reached the dataset's end. |
+| `packageInfo` | Core and consumer package names and versions. |
+
+### Methods
+
+| Method | Purpose |
+| --- | --- |
+| `relax` | Wait until the scroller is idle. |
+| `reload` | Reload data at an optional starting index, keeping the current configuration. |
+| `reset` | Restart the scroller with optional datasource and settings changes. |
+| `pause`, `resume` | Suspend or resume workflow processing. |
+| `append`, `prepend` | Add items after or before the known range. |
+| `insert` | Insert items before or after a target item. |
+| `remove` | Remove selected items by predicate or indexes. |
+| `replace` | Replace matching buffered items with a new set of items. |
+| `update` | Keep, remove or replace buffered items using a callback. |
+| `check` | Re-measure rendered items after their sizes change. |
+| `clip` | Trim offscreen buffer items beyond the configured padding. |
+| `fix` | Directly adjust scroll position, index bounds or items. Experimental. |
+| `showLog` | Print collected debug logs. |
+
+## Documentation
+
+See the [documentation index](docs/index.md) for a guided path through the reference pages below.
+
+- **Core integration**
+ - [Virtual scrolling model](docs/virtual-scrolling.md) — understand how the viewport, item buffer and DOM rows fit together.
+ - [Workflow and lifecycle](docs/workflow.md) — construct, dispose and recreate an integration.
+ - [Datasource](docs/datasource.md) — provide data, handle failures and cache items.
+ - [Rendering](docs/rendering.md) — implement the consumer's DOM and rendering contract.
+
+- **Configuration and extensions**
+ - [Configuration](docs/configuration.md) — configure sizing, buffering, scrolling and diagnostics.
+ - [Adapter properties](docs/adapter.md) — inspect workflow state and visible items.
+ - [Adapter methods](docs/adapter-methods.md) — control the scroller and modify buffered items.
+ - [Custom Routines](docs/routines.md) — customize DOM operations and scheduling.
+
+- **Help**
+ - [Troubleshooting](docs/troubleshooting.md) — diagnose integration problems and use debug logs.
## Thanks
- \- to [Mike Feingold](https://github.com/mfeingold) as he started all this story in far 2013,
-
- \- to [Joshua Toenyes](https://github.com/JoshuaToenyes) as he transferred ownership to the "vscroll" npm repository which he owned but did not use,
-
- \- to all contributors of related repositories ([link](https://github.com/angular-ui/ui-scroll/graphs/contributors), [link](https://github.com/dhilt/ngx-ui-scroll/graphs/contributors)),
-
- \- to all donators as their great support does increase motivation.
-
-
+- To [Mike Feingold](https://github.com/mfeingold), who started this project family in 2013.
+- To [Joshua Toenyes](https://github.com/JoshuaToenyes), who transferred ownership of the vscroll npm package name.
+- To all contributors to [ui-scroll](https://github.com/angular-ui/ui-scroll/graphs/contributors) and [ngx-ui-scroll](https://github.com/dhilt/ngx-ui-scroll/graphs/contributors).
+- To everyone supporting the project through donations.
- __________
+---
-2026 © [Denis Hilt](https://github.com/dhilt)
+2026 © [Denis Hilt](https://github.com/dhilt) · [MIT license](LICENSE)
diff --git a/docs/adapter-methods.md b/docs/adapter-methods.md
new file mode 100644
index 00000000..e585238c
--- /dev/null
+++ b/docs/adapter-methods.md
@@ -0,0 +1,110 @@
+# Adapter methods
+
+[← Adapter properties](adapter.md) · [Documentation index](index.md)
+
+The Adapter exposes methods for working with a running scroller: waiting for it to settle, reloading data, and changing buffered items. The table below lists their arguments and behavior.
+
+| Method | Arguments | Description |
+| --- | --- | --- |
+| `relax` | callback?: () => void | Wait until the scroller is idle. If it is already idle, `callback` may run synchronously before the returned Promise resolves; otherwise it runs when the scroller settles. A reload, reset, or disposal can end a pending wait with `success: false` without calling `callback`. Idle does not imply that a datasource request succeeded. |
+| `reload` | reloadIndex?: number | string | Clear the Buffer, reset the scroll position, and load data starting at `reloadIndex`. If omitted or invalid, the index falls back to `settings.startIndex` (default `1`), within configured bounds; the setting itself is unchanged. An active cycle may be interrupted, but the call completes without performing the operation while paused. The internal cache of item indexes and measured sizes is cleared unless `devSettings.cacheOnReload` is enabled. |
+| `reset` | datasource?: {
get?: function;
settings?: object;
devSettings?: object;
`}` | Rebuild the scroller and start a new load. With no argument, reuse the current datasource configuration. Supplied `get`, `settings`, or `devSettings` replace the corresponding sections; omitted sections remain, but individual settings omitted from a supplied `settings` object return to their defaults. The Buffer and index-and-size cache are cleared even with `cacheOnReload`. An active cycle can be interrupted; reset also works while paused and resumes processing. The public datasource and Adapter remain the same objects. See [Datasource](datasource.md) for `get` signatures and [Configuration](configuration.md) for settings. |
+| `pause` | — | Pause scroller processing without clearing the Buffer. While paused, viewport scroll events do not trigger processing and most Adapter calls complete without performing an operation; `resume()` and `reset()` remain available. |
+| `resume` | — | Clear the paused state and start a scroller cycle. A cycle also starts if the scroller was not paused. |
+| `check` | — | Remeasure rendered rows after a DOM or layout change outside normal scroller processing. If their sizes changed, update size estimates and adjust scrolling; fetching or rendering may follow. The DOM change must be committed before `check()` runs. |
+| `clip` | options?: {
forwardOnly?: boolean;
backwardOnly?: boolean;
`}` | Remove buffered rows beyond the visible area and configured padding, replacing their space with virtual padding; datasource items are not deleted. By default, both sides are eligible for clipping. `forwardOnly` limits clipping to the higher-index side; `backwardOnly` limits it to the lower-index side. The two flags cannot be combined. This also works in `infinite` mode. |
+| `append` | options: {
items: Data[];
eof?: boolean;
decrease?: boolean;
virtualize?: boolean;
`}` | Add nonempty `items` after the highest cached index, rendering them if they adjoin the current Buffer; otherwise they remain virtual until fetched. `eof: true` instead inserts at the known dataset end and renders only when the Buffer reaches it. `virtualize: true` keeps the items virtual even there; it cannot be combined with `eof`. Later indexes increase by default; `decrease: true` fixes the upper boundary and decreases preceding indexes instead. Input order is preserved. An empty Buffer is filled directly. |
+| `prepend` | options: {
items: Data[];
bof?: boolean;
increase?: boolean;
virtualize?: boolean;
`}` | Add nonempty `items` before the lowest cached index, rendering them if they adjoin the current Buffer; otherwise they remain virtual until fetched. `bof: true` instead inserts at the known dataset beginning and renders only when the Buffer reaches it. `virtualize: true` keeps the items virtual even there; it cannot be combined with `bof`. Earlier indexes decrease by default; `increase: true` fixes the lower boundary and increases later indexes instead. Input order is reversed. An empty Buffer is filled directly. |
+| `insert` | options: {
items: Data[];
before?: (item) => boolean;
after?: (item) => boolean;
beforeIndex?: number;
afterIndex?: number;
decrease?: boolean;
`}` | Insert a nonempty `items` array before or after a target. Exactly one of `before`, `after`, `beforeIndex`, or `afterIndex` is required. The `before` and `after` callbacks select the first matching buffered item; `beforeIndex` and `afterIndex` can also target virtual positions within known dataset bounds. With an empty Buffer, use `beforeIndex` or `afterIndex`. Input order is preserved. Later indexes increase by default; `decrease: true` decreases preceding indexes instead. If no target is found, no insertion occurs. |
+| `remove` | options: {
predicate?: (item) => boolean;
indexes?: number[];
increase?: boolean;
`}` | Remove buffered or eligible virtual items. The `predicate` callback tests each buffered item and selects those for which it returns `true`; `indexes` can also target virtual positions. Exactly one of `predicate` or `indexes` is required; indexes should be distinct integers. Following indexes decrease by default; `increase: true` increases preceding indexes instead. If nothing matches, the indexed data remains unchanged. |
+| `replace` | options: {
items: Data[];
predicate: (item) => boolean;
fixRight?: boolean;
`}` | Replace buffered items with new data. The `predicate` callback tests each buffered item and should select one contiguous block; `items` supplies the replacements. Virtual items cannot be selected. By default, the lower side stays fixed and later indexes absorb a change in item count. `fixRight: true` fixes the upper side and shifts preceding indexes instead. If `predicate` matches nothing, the indexed data remains unchanged. |
+| `update` | options: {
predicate: (item) => unknown;
fixRight?: boolean;
`}` | Modify the current Buffer by keeping, removing, or replacing items. The `predicate` callback runs synchronously for each buffered item; its result determines the action: a falsy value or `[]` removes it; a truthy non-array value keeps it; a nonempty array replaces it with those data values. Returning an object alone does not replace `item.data`. If an array contains the original `item.data` by identity, that entry retains the existing item and must not be repeated; other values create new items. The callback is not awaited. By default, the lower boundary stays fixed and changes in item count shift later indexes. `fixRight: true` fixes the upper boundary instead and processes items from high to low. |
+| `fix` | options: {
scrollPosition?: number;
minIndex?: number;
maxIndex?: number;
updater?: (item, update) => void;
scrollToItem?: (item) => boolean;
scrollToItemOpt?: boolean | ScrollIntoViewOptions;
`}` | Experimental direct adjustments. `scrollPosition` sets an integer pixel offset; `-Infinity` and `Infinity` select the beginning and end. `minIndex` and `maxIndex` change dataset bounds without fetching or removing buffered items. `updater` visits buffered items; calling its `update()` argument requests a new Buffer reference and render. `scrollToItem` finds the first matching buffered item without fetching, and `scrollToItemOpt` configures how it is scrolled into view. Combined options run in the order shown. The result does not wait for a cycle caused by scrolling. |
+| `showLog` | — | Print queued diagnostic messages when `devSettings.debug` is enabled and `immediateLog` is disabled. With `immediateLog`, diagnostics are printed as they occur. Unlike the other methods, this one does not return a Promise. |
+
+## Calling methods
+
+Adapter methods can trigger several internal scroller processes, including data fetching, rendering, and viewport adjustment. Some of these processes may be asynchronous. All methods except `showLog()` return a Promise that resolves when the method has finished its work, providing a result object with the following fields:
+
+| Field | Type | Meaning |
+| --- | --- | --- |
+| `success` | `boolean` | `true` — successful completion of the Adapter call; `false` — Adapter error or interruption. |
+| `immediate` | `boolean` | `true` for immediate completion. `false` does not necessarily mean asynchronous execution. The result is always returned through a Promise. |
+| `details` | `string \| null` | Reason for an Adapter error, interruption, or completion without performing an operation; otherwise `null`. |
+
+Reported errors during data fetching or rendering are recorded in `workflow.errors`, but the Adapter call may still return `success: true` and `details: null`.
+
+Adapter methods are available as `datasource.adapter` immediately after datasource instantiation from the class returned by `makeDatasource()`. Before `Workflow` initializes the Adapter, calls complete without performing an operation and return `success: true`, `immediate: true`, and `details: 'Adapter is not initialized'`.
+
+```js
+const Datasource = makeDatasource();
+const datasource = new Datasource({ get });
+const adapter = datasource.adapter;
+
+const before = await adapter.relax();
+console.log(before); // { success: true, immediate: true, details: 'Adapter is not initialized' }
+const initialized = new Promise(resolve => adapter.init$.once(resolve));
+
+new Workflow({ consumer, element, datasource, run }); // Workflow init -> Adapter init
+await initialized;
+const after = await adapter.relax();
+console.log(after); // { success: true, immediate: false | true, details: null }
+```
+
+When the scroller is paused through `adapter.pause()`, most Adapter method calls likewise complete without performing an operation, returning `success: true`, `immediate: true`, and `details: 'Scroller is paused'`. The methods `reset()`, `resume()`, `relax()`, and `showLog()` remain available.
+
+Correct sequencing matters in chains of Adapter calls. If an operation depends on the previous one finishing, its call must follow the resolution of the previous method's Promise:
+
+```js
+await adapter.reload();
+await adapter.append({ items });
+```
+
+`reload()` and `reset()` suppress results from superseded `get` calls but do not cancel the underlying external requests. Transport cancellation is the integration's responsibility.
+
+## Callbacks and items
+
+Some Adapter methods take callbacks that select or modify items in the current Buffer. These callbacks receive an `item` of type `IAdapterItem`, abbreviated as `(item)` in the methods table. It contains the following fields:
+
+| Field | Type | Meaning |
+| --- | --- | --- |
+| `data` | `Data` | Original application data. |
+| `$index` | `number` | Current item position in the dataset. |
+| `uid` | `number` | Item instance identifier, retained when its index changes. |
+| `element` | `HTMLElement` | Reference to the DOM row, if already available. Optional field. |
+
+For example, the `predicate` parameter of `remove()` selects items for removal by the `id` field in their application data:
+
+```js
+await adapter.remove({ predicate: item => item.data.id === idToRemove });
+```
+
+The callbacks `before`, `after`, `predicate`, and `scrollToItem` must declare exactly one parameter and return synchronously; returned Promises are not awaited. Selection callbacks return `boolean`; the `predicate` of `update()` determines item changes as described in the methods table. The `fix.updater` callback may declare one or two parameters; the second is an `update()` function of type `() => void`.
+
+Unlike callback arguments, the `items` parameter takes original application data (`Data[]`), not Buffer items. It must be a nonempty array of values with the same JavaScript type (`typeof`).
+
+## Data consistency
+
+Data consistency means that scroller state and `datasource.get` responses reflect the same records in the same order. Adapter insertions, removals, replacements, and updates change scroller state; matching changes in the application's data source are handled by the integration. Otherwise, a later range request after scrolling or `reload()` may lose added records or bring removed ones back.
+
+The sequence is `relax()` → update the data source → call the Adapter method. The method itself may trigger a new `get` call, so the source must already reflect the change. `relax()` waits for current work to finish; it does not lock the scroller against future work.
+
+For example, the datasource reads an array with indexes starting at `1`, and `removeC()` removes the record with `id: 'c'` after scroller initialization:
+
+```js
+let DATA = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
+const datasource = new Datasource({
+ get: (index, count, done) => done(DATA.slice(index - 1, index - 1 + count)),
+ settings: { startIndex: 1, minIndex: 1 }
+});
+
+async function removeC() {
+ await datasource.adapter.relax();
+ DATA = DATA.filter(data => data.id !== 'c'); // Sync the change at the datasource level
+ await datasource.adapter.remove({ predicate: ({ data }) => data.id === 'c' });
+}
+```
+
+After removal, the record with `id: 'c'` is absent from both the Buffer and `get` responses. A stable record identifier can be stored in `item.data`; in contrast, `item.$index` denotes its current position and can change after insertions or removals.
+
+Index shifts in the source must match the strategy selected through `increase`, `decrease`, or `fixRight`. This also applies to virtual operations on records outside the Buffer. Explicit dataset bounds and any datasource cache must remain consistent with the changes.
diff --git a/docs/adapter.md b/docs/adapter.md
new file mode 100644
index 00000000..be292d4e
--- /dev/null
+++ b/docs/adapter.md
@@ -0,0 +1,75 @@
+# Adapter API
+
+[← Documentation index](index.md) · [Adapter methods →](adapter-methods.md)
+
+The Adapter adds runtime observation and control to virtual scrolling. It exposes workflow state, visible items and dataset boundaries, and supports data changes without recreating `Workflow`. It is available as `datasource.adapter` when the datasource is constructed through [`makeDatasource()`](datasource.md#creating-a-datasource-with-an-adapter). The Adapter exists at datasource construction; scroller-control methods take effect after Workflow initialization.
+
+## Properties
+
+Adapter properties are read-only. A property ending in `$` is the reactive counterpart of its scalar property; the core subscription API is described [below](#reactive-subscriptions).
+
+| Property | Type | Meaning |
+| --- | --- | --- |
+| `init`, `init$` | `boolean`, reactive boolean | Becomes `true` when `Workflow` connects the Adapter to the scroller. This does not signal completion of the first load. |
+| `isLoading`, `isLoading$` | `boolean`, reactive boolean | Indicates whether the scroller is busy with a Workflow cycle. [Details below](#cycles-and-inner-loops). |
+| `loopPending`, `loopPending$` | `boolean`, reactive boolean | Indicates whether a Workflow cycle is running an inner loop (fetch/render/adjust). [Details below](#cycles-and-inner-loops). |
+| `itemsCount` | `number` | Number of non-invisible Buffer items, including offscreen overscan; not the dataset total. |
+| `bufferInfo` | `IBufferInfo` | Summarizes the Buffer range, cache, and dataset boundaries. [Details below](#bufferinfo). |
+| `firstVisible`, `firstVisible$` | `IAdapterItem`, reactive item | First item visible in the viewport, even partially. |
+| `lastVisible`, `lastVisible$` | `IAdapterItem`, reactive item | Last item visible in the viewport, even partially. |
+| `bof`, `bof$` | `boolean`, reactive boolean | Indicates whether the Buffer has reached the known beginning of the dataset. |
+| `eof`, `eof$` | `boolean`, reactive boolean | Indicates whether the Buffer has reached the known end of the dataset. |
+| `paused`, `paused$` | `boolean`, reactive boolean | Indicates whether Workflow processing is paused. |
+| `packageInfo` | object | Core and consumer `name`/`version` metadata. |
+| `version` | `string` | Core version associated with this Adapter context. |
+
+### Reactive subscriptions
+
+The built-in `$` properties provide `get()` for the current value, `on(callback)` for updates, and `once(callback)` for one notification. Both subscription methods return a cancellation function.
+
+Boolean properties do not emit their current value on subscription. A listener installed before `Workflow` construction observes the initial loading cycle without an extra read. Here, `#loading-indicator` starts hidden in the markup:
+
+```js
+const indicator = document.getElementById('loading-indicator');
+const off = datasource.adapter.isLoading$.on(loading => {
+ indicator.hidden = !loading;
+});
+new Workflow({ consumer, element, datasource, run });
+
+// When the integration is removed:
+off();
+```
+
+For a listener installed after the scroller starts, read the scalar value as well to initialize the indicator.
+
+Notifications are synchronous, and identical (`===`) values are not emitted again. `firstVisible$` and `lastVisible$` differ from the boolean properties: they emit their current value on subscription, possibly `EMPTY_ITEM` before an item is visible. Thus, `once()` on either does not necessarily wait for a visible item. Reactive properties report Adapter state; they do not control it. A [custom reactive configuration](datasource.md#consumer-specific-adapter-reactivity) may replace the built-in subscription API.
+
+### Cycles and inner loops
+
+A Workflow cycle is a processing session started by initialization, a relevant scroll event, or an Adapter operation. It remains active until the scroller has finished the resulting work; `isLoading` marks this whole interval. One cycle may contain several inner loops.
+
+An inner workflow loop is one pass through the needed work: determining missing data, fetching it when needed, rendering and measuring rows, clipping the Buffer, and adjusting scrolling geometry. Some steps may be skipped. If a pass reveals more work—for example, the rendered rows are shorter than estimated—another loop follows within the same cycle. `loopPending` marks each individual pass.
+
+Cycles and inner loops are fundamental to VScroll's internal architecture. The [Workflow wiki page](https://github.com/dhilt/vscroll/wiki/VScroll-Workflow) provides detailed flowcharts of both.
+
+### bufferInfo
+
+`bufferInfo` provides a snapshot of the current Buffer state, computed on access rather than updated in place.
+
+| Field | Meaning |
+| --- | --- |
+| `firstIndex`, `lastIndex` | Lowest and highest indexes in the current Buffer; `NaN` when it is empty or before initialization. |
+| `minIndex`, `maxIndex` | Lowest and highest cached indexes, including previously rendered items; `NaN` before initialization, or `startIndex` when the initialized cache is empty. |
+| `absMinIndex`, `absMaxIndex` | Known absolute dataset boundaries, supplied by settings or inferred from datasource responses; Adapter operations may change them. Unknown bounds remain infinite. Before initialization, they are `-Infinity` and `Infinity`, respectively. |
+| `defaultSize` | Current estimated item size where an individual size is unknown; `NaN` before initialization. |
+
+### Visible items
+
+`firstVisible` and `lastVisible` identify the first and last items intersecting the viewport, including partially visible rows. Unlike the Buffer's edges, these delimit the visible range. When both are available, the visible item count cannot exceed `itemsCount`, which also includes offscreen buffered items:
+
+```js
+const visibleCount = adapter.lastVisible.$index - adapter.firstVisible.$index + 1;
+expect(visibleCount).toBeLessThanOrEqual(adapter.itemsCount);
+```
+
+Tracking of each edge begins when its scalar or `$` property is first accessed. Until a visible item is available, the value is `EMPTY_ITEM`, not an item with a usable `$index` or DOM element. The reactive properties report changes to the visible edges, not every scroll event or in-place change to an item's data.
diff --git a/docs/assets/datasource-flow.png b/docs/assets/datasource-flow.png
new file mode 100644
index 00000000..3eae383a
Binary files /dev/null and b/docs/assets/datasource-flow.png differ
diff --git a/docs/assets/viewport-animation.gif b/docs/assets/viewport-animation.gif
new file mode 100644
index 00000000..cba4f0f7
Binary files /dev/null and b/docs/assets/viewport-animation.gif differ
diff --git a/docs/assets/viewport-static.png b/docs/assets/viewport-static.png
new file mode 100644
index 00000000..c6ea36fe
Binary files /dev/null and b/docs/assets/viewport-static.png differ
diff --git a/docs/assets/vscroll-distribution.png b/docs/assets/vscroll-distribution.png
new file mode 100644
index 00000000..06557092
Binary files /dev/null and b/docs/assets/vscroll-distribution.png differ
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 00000000..350c646e
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,107 @@
+# Configuration
+
+[← Documentation index](index.md)
+
+Configuration is supplied through the [datasource](datasource.md) passed to `Workflow`. Its optional `settings` object controls scrolling; `devSettings` controls diagnostics and lower-level behavior. Both are read when the scroller is created. Sizes below are CSS pixels along the scrolling axis, and delays are milliseconds.
+
+```js
+const datasource = new Datasource({
+ get,
+ settings: { startIndex: 1, padding: 0.5 },
+ devSettings: { debug: true }
+});
+```
+
+## Settings
+
+### Bounds and initial positioning
+
+The scroller requests items by consecutive integer indexes. Bounds describe the available dataset, while `startIndex` selects the initial position.
+
+| Setting | Type | Default | Effect |
+| --- | --- | --- | --- |
+| `startIndex` | Integer | `1` | Initial index, clamped to the bounds; zero and negative indexes are valid. |
+| `minIndex` | Integer or infinity | `-Infinity` | Inclusive lower bound. |
+| `maxIndex` | Integer or infinity | `Infinity` | Inclusive upper bound. |
+
+For a nonempty dataset, use `minIndex <= startIndex <= maxIndex`. Unknown ends are discovered through [short responses](datasource.md#data-ranges-and-boundaries). Indexes are positions, not permanent record IDs.
+
+Bounds also shape the scrollbar. With neither bound known, its range reflects the items discovered so far and grows as more data is fetched. A known bound lets the scroller estimate virtual space on that side; with both bounds known, the scrollbar represents the estimated extent of the full dataset from the initial load. The scrollable range and thumb size can still change as rendered rows are measured.
+
+### Buffering and scrolling mode
+
+The buffer includes visible rows and a margin of offscreen rows. These settings determine how much is requested and retained around the viewport.
+
+| Setting | Type | Default | Effect |
+| --- | --- | --- | --- |
+| `bufferSize` | Integer ≥ `1` | `5` | Minimum fetch batch target, not a limit on buffered rows or a fixed `get` count. |
+| `padding` | Number ≥ `0.01` | `0.5` | Target offscreen content on each side, measured in viewport sizes. `0.5` means roughly half a viewport per side. |
+| `infinite` | `boolean` | `false` | Disables automatic clipping, so loaded rows remain in the buffer and DOM. |
+
+Increasing `padding` or `bufferSize` can reduce fetch frequency at the cost of more rendered content. In infinite mode, explicit [Adapter `clip()`](adapter-methods.md#clip) is still available.
+
+### Size estimates and layout
+
+Before the first render, the scroller estimates how many rows to request for the viewport and its outlets. Without `itemSize`, the first request uses `bufferSize` as its batch target.
+
+| Setting | Type | Default | Effect |
+| --- | --- | --- | --- |
+| `itemSize` | Integer ≥ `1` | `NaN` (unknown) | Initial row-height estimate, or row-width estimate in horizontal mode, used to size the first request and estimate virtual space. It does not set a CSS size. |
+| `sizeStrategy` | `SizeStrategy` | `'average'` | Estimate unknown rows using an average, the most frequent measured size, or a constant estimate: `'average'`, `'frequent'`, or `'constant'`. |
+
+After rendering, the scroller measures actual row sizes. With `'average'` (the default) or `'frequent'`, later requests use those measurements rather than the initial `itemSize` estimate. With `'constant'`, `itemSize` remains the estimate for unseen rows; if omitted, the first measured size takes its place. No strategy forces equal CSS sizes. In TypeScript, use the exported `SizeStrategy` enum (for example, `SizeStrategy.Frequent`). See [Rendering](rendering.md) for the DOM and layout requirements.
+
+### Viewport and horizontal scrolling
+
+The viewport is the scrollable area whose position and size the scroller tracks; by default, it is the content element's parent. `windowViewport` and `viewportElement` choose a different scroll target, while `horizontal` changes the scroll axis.
+
+| Setting | Type | Default | Effect |
+| --- | --- | --- | --- |
+| `windowViewport` | `boolean` | `false` | Use the browser window for scrolling and viewport dimensions. |
+| `viewportElement` | `HTMLElement`, synchronous function returning one, or `null` | `null` | Use an explicit viewport instead of the content element's parent. Experimental. |
+| `horizontal` | `boolean` | `false` | Scroll and measure along the horizontal axis; the DOM/CSS layout must also be horizontal. |
+
+`windowViewport` takes precedence over `viewportElement`. A viewport factory is evaluated when the scroller is created; it must return an element synchronously. An invalid result falls back to the parent viewport.
+
+### Other settings
+
+| Setting | Type | Default | Effect |
+| --- | --- | --- | --- |
+| `inverse` | `boolean` | `false` | Align content shorter than the viewport to the bottom/right without reversing item order. Experimental. |
+| `onBeforeClip` | `(items: IAdapterItem[]) => void` or `null` | `null` | Called synchronously after clipped rows are hidden, before their items leave the buffer. Experimental. |
+
+## Development settings
+
+`devSettings` exposes diagnostic logging as well as timing, cache, and browser-behavior controls. The [development log](https://github.com/dhilt/vscroll/wiki/Dev-Log) explains how a trace reveals workflow cycles, data fetches, rendering, clipping, and scroll adjustments. For a focused trace, set `devSettings: { debug: true, immediateLog: false }` and call `datasource.adapter.showLog()` after the activity of interest; this requires an [Adapter-enabled datasource](datasource.md#creating-a-datasource-with-an-adapter).
+
+### Diagnostics
+
+| Setting | Type | Default | Effect |
+| --- | --- | --- | --- |
+| `debug` | `boolean` | `false` | Enable core logging; the other logging options have no effect without it. |
+| `immediateLog` | `boolean` | `true` | Print messages immediately. If false, keep them in memory until [Adapter `showLog()`](adapter-methods.md#showlog) flushes them. |
+| `logProcessRun` | `boolean` | `false` | Include process fire/run events. |
+| `logTime` | `boolean` | `false` | Include elapsed workflow time. |
+| `logColor` | `boolean` | `true` | Apply console colors to logs. |
+
+### Timing, caching, and browser behavior
+
+| Setting | Type | Default | Effect |
+| --- | --- | --- | --- |
+| `throttle` | Integer ≥ `0` | `40` | Throttle scroll-event handling in milliseconds. |
+| `initDelay` | Integer ≥ `0` | `1` | Base delay before workflow initialization, in milliseconds. |
+| `initWindowDelay` | Integer ≥ `0` | `40` | Initialization-delay candidate in window mode when `history.scrollRestoration` is unavailable; the larger applicable delay is used. |
+| `cacheData` | `boolean` | `false` | Retain item data with measured sizes and indexes in the internal cache; it does not answer `get` requests. |
+| `cacheOnReload` | `boolean` | `false` | Retain the internal cache and size estimate across [Adapter `reload()`](adapter-methods.md#reload), not rendered rows. |
+| `dismissOverflowAnchor` | `boolean` | `true` | Disable native overflow anchoring on the viewport so it does not compete with scroller adjustments. |
+| `directionPriority` | `'backward'` or `'forward'` | `'backward'` | Choose which side stays fixed when measured sizes change: usually top/left; `'forward'` can anchor bottom/right when fetching before retained items. |
+
+Retained cache entries are useful only while their indexes still refer to the same data and layout. They are separate from an [application-side cache of datasource requests](datasource.md#caching-datasource-requests).
+
+With native routines, window mode sets `history.scrollRestoration` to `'manual'` where supported, and `dismissOverflowAnchor` sets an inline viewport style. Workflow disposal does not restore these browser settings.
+
+## Validation and applying changes
+
+All fields are optional. Missing or invalid values take their defaults; unknown fields are ignored. Valid numbers below a listed minimum are clamped to it. For example, `bufferSize: 0` becomes `1`, while a fractional `bufferSize` is invalid and falls back to `5`. TypeScript expects numbers and booleans, although runtime validation also accepts numeric strings and the exact strings `'true'` and `'false'`.
+
+Configuration is read during scroller construction: changing the original object does not update a running scroller. [Adapter `reload()`](adapter-methods.md#reload) retains its settings; [Adapter `reset()`](adapter-methods.md#reset) can supply new datasource configuration. A supplied `settings` or `devSettings` section replaces that section rather than merging individual fields, so include the options that must be retained. For TypeScript, the datasource can be typed as `IDatasource`; `Settings` and `DevSettings` are not package-root exports.
diff --git a/docs/datasource.md b/docs/datasource.md
new file mode 100644
index 00000000..f5e32a7b
--- /dev/null
+++ b/docs/datasource.md
@@ -0,0 +1,145 @@
+# Datasource
+
+[← Documentation index](index.md)
+
+A datasource is the application-provided object from which vscroll obtains data. During initialization and as scrolling reveals missing items, the Scroller calls its `get` function. The application supplies values; vscroll adds them to its item buffer and passes that buffer to the consumer's `run(items)` for [rendering](rendering.md). The datasource itself does not render DOM.
+
+
+
+## Data ranges and boundaries
+
+Every [Workflow](workflow.md) needs a datasource object with `get`. It may also have `settings` and `devSettings`, documented in [Configuration](configuration.md). A plain object is enough for ordinary scrolling. This minimal callback example supplies synchronous, infinite data:
+
+```js
+const datasource = {
+ get: (index, count, success) =>
+ success(Array.from({ length: count }, (_, offset) => ({
+ text: `Item ${index + offset}`
+ })))
+};
+```
+
+`get(index, count)` requests the consecutive indexes from `index` through `index + count - 1`, inclusive. Indexes may be zero or negative. Requests can move in either direction, vary in size, and repeat earlier ranges. The response must contain no more than `count` values, ordered by increasing index with no gaps. These are application values, not VScroll items: vscroll assigns their `$index` from their positions in the response, not from an application `id` field.
+
+A finite datasource can read from an application collection. In this example, datasource indexes start at `1` and map to the array's zero-based offsets:
+
+```js
+const MIN_INDEX = 1;
+const records = [
+ { id: 'a', text: 'Alpha' },
+ { id: 'b', text: 'Beta' },
+ { id: 'c', text: 'Gamma' }
+];
+
+const datasource = {
+ get(index, count, success) {
+ const start = index - MIN_INDEX;
+ success(records.slice(start, start + count));
+ },
+ settings: {
+ startIndex: MIN_INDEX,
+ minIndex: MIN_INDEX,
+ maxIndex: MIN_INDEX + records.length - 1
+ }
+};
+```
+
+A short response signals a dataset boundary, not a partially loaded page. For a forward request it establishes the upper boundary; for a backward request, the available values are placed immediately before the existing buffer and establish the lower boundary. For example, on a backward request after the buffer exists, if the requested range is `-2..2` but the dataset begins at `1`, the response contains the values for `1..2`, in that order. An empty response means no data in that direction; an empty initial response marks both boundaries for the current dataset.
+
+The first request is special: if its response is short, vscroll indexes those values starting at the effective `startIndex`. For a nonempty dataset, `startIndex` must refer to an existing item. Known inclusive `minIndex` and `maxIndex` bounds can be set in [settings](configuration.md#settings); otherwise short responses reveal the edges. `bufferSize` is only a request-size target, not a fixed `count` or a limit on rendered rows.
+
+## Asynchronous delivery and errors
+
+When data comes from an asynchronous service, `get` can return its Promise directly. Here `dataService.readAsync(index, count)` is supplied by the application and resolves to an ordered array that follows the same range contract:
+
+```js
+const datasource = {
+ get: (index, count) => dataService.readAsync(index, count)
+};
+```
+
+`get` should use one delivery style. Returning an array directly or combining a callback with a returned Promise or Observable is unsupported. Each request must settle once:
+
+| Style | How to deliver data or failure |
+| --- | --- |
+| Callback | Data or errors are delivered through `success(data)` or `fail(error)`, synchronously or later. Core supplies `fail`, although its TypeScript parameter is optional. |
+| Promise-like | `get` returns a Promise resolving to `data` or rejecting with an error; an `async get(index, count)` works. |
+| Observable-like | `get` returns an object with `subscribe(next, error, complete)`; one array is emitted through `next` or an error through `error`. |
+
+An Observable-like source is a one-request delivery mechanism, not a live stream of list changes. Completing without an array or error leaves the request pending. Its subscription must provide `unsubscribe()`, but core does not unsubscribe on cancellation or error; the source owns that cleanup. The current TypeScript Observable declaration has an extra array nesting; the runtime expects `Data[]`, not `Data[][]`. A callback or Promise adapter avoids this type mismatch.
+
+Failures are reported through `fail`, Promise rejection or Observable error. A synchronous exception thrown by `get` is not converted into a fetch failure. An error must not be represented by `[]`: that tells the Scroller it has reached a boundary. vscroll does not retry automatically; the application handles errors and decides whether to retry.
+
+`get` must declare both `index` and `count` as parameters: runtime validation requires `get.length >= 2`. A custom class method needs binding or an arrow property because vscroll calls it without binding `this`.
+
+## Caching datasource requests
+
+As the viewport moves, the Scroller may request ranges that overlap earlier requests. Its internal cache stores measured sizes and, optionally, item data, but does not serve `get` responses. For an unbounded dataset whose service returns every requested item, a datasource can cache items by index and fetch only missing spans:
+
+```js
+const cache = new Map();
+const datasource = {
+ get: async (index, count) => {
+ const result = [];
+ const end = index + count;
+ let current = index;
+ while (current < end) {
+ if (cache.has(current)) {
+ result.push(cache.get(current));
+ current++;
+ continue;
+ }
+ let next = current + 1;
+ while (next < end && !cache.has(next)) next++;
+ const missing = await dataService.readAsync(current, next - current);
+ missing.forEach((item, offset) => cache.set(current + offset, item));
+ result.push(...missing);
+ current = next;
+ }
+ return result;
+ }
+};
+```
+
+For a bounded dataset, a short response must be cached under the items' actual indexes, taking known boundaries into account. The cache must be invalidated when data or index assignments change, and its size should be limited for long-running lists.
+
+## Creating a datasource with an Adapter
+
+A datasource can also expose the [Adapter API](adapter.md), which lets an application observe and control a running scroller—for example, track loading state, reload data, or update items. For the API to be available as `datasource.adapter`, the datasource must be an instance of the class returned by `makeDatasource()`:
+
+```js
+import { makeDatasource } from 'vscroll';
+
+const Datasource = makeDatasource();
+const datasource = new Datasource({
+ get: (index, count) => dataService.readAsync(index, count)
+});
+const adapter = datasource.adapter;
+```
+
+The resulting datasource serves as the `datasource` argument to `Workflow`. Reactive Adapter properties can be observed immediately; methods that control the scroller take effect only after [Workflow initialization](adapter-methods.md#calling-methods).
+
+For TypeScript, `IDatasource` describes the general shape and `IDatasourceConstructed` guarantees the Adapter property. Both interfaces are exported from `vscroll`.
+
+## Consumer-specific Adapter reactivity
+
+`makeDatasource(getAdapterConfig?)` can replace individual Adapter reactive properties for a framework integration. The optional factory runs once per datasource instance and returns `IAdapterConfig`: `{ mock: boolean, reactive?: ... }`. Each reactive entry, keyed by an `AdapterPropName`, supplies `{ source, emit(source, value) }`. The Adapter exposes `source` as that property and calls `emit` when its value changes. Unconfigured properties keep native reactivity; a supplied configuration does not merge in the default factory configuration.
+
+```ts
+import { AdapterPropName, makeDatasource } from 'vscroll';
+
+const Datasource = makeDatasource(() => ({
+ mock: false,
+ reactive: {
+ [AdapterPropName.isLoading$]: {
+ source: new EventTarget(),
+ emit(source, value) {
+ (source as EventTarget).dispatchEvent(new CustomEvent('change', { detail: value }));
+ }
+ }
+ }
+}));
+const reactiveDatasource = new Datasource({ get });
+```
+
+Here `reactiveDatasource.adapter.isLoading$` is an `EventTarget` at runtime, not the default reactive object; subscriptions use `addEventListener`, not `.on()`. In TypeScript, this property needs a narrower or consumer-specific Adapter type, since the default type still describes native reactivity. Each instance needs a fresh `source`, and consumer-owned listeners must be released when its view is destroyed. The [ngx-ui-scroll datasource bridge](https://github.com/dhilt/ngx-ui-scroll/blob/master/scroller/src/ui-scroll.datasource.ts) demonstrates custom Adapter reactivity in a framework integration.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..62113da7
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,23 @@
+# Documentation
+
+[← Project README](../README.md)
+
+Read the first four pages for the core integration contract, then use the remaining references as needed.
+
+## Core integration
+
+1. [Virtual scrolling model](virtual-scrolling.md) — viewport, virtual space and the path from datasource to DOM.
+2. [Workflow and lifecycle](workflow.md) — construction, disposal and recreation.
+3. [Datasource](datasource.md) — indexed data requests, delivery and errors.
+4. [Rendering and DOM contract](rendering.md) — the `run` callback, required DOM and render timing.
+
+## Configuration and extensions
+
+5. [Configuration](configuration.md) — scrolling settings and development settings.
+6. [Adapter properties](adapter.md) — observe workflow state, visible items and boundaries.
+7. [Adapter methods](adapter-methods.md) — control the scroller and modify buffered items.
+8. [Custom Routines](routines.md) — optional DOM and scheduling overrides.
+
+## Help
+
+9. [Troubleshooting](troubleshooting.md) — common failures, logs and bug reports.
diff --git a/docs/rendering.md b/docs/rendering.md
new file mode 100644
index 00000000..80b2c553
--- /dev/null
+++ b/docs/rendering.md
@@ -0,0 +1,92 @@
+# Rendering and DOM contract
+
+[← Documentation index](index.md)
+
+The rendering integration is established when `Workflow` is instantiated. Two constructor parameters define its core contract: `element` identifies the mounted content element, and `run(items)` keeps its rows aligned with the Scroller's item buffer.
+
+## Required DOM and layout
+
+The DOM must provide a scrollable viewport with a constrained size. Inside it, a content element holds two empty padding elements, one before and one after the item rows. This structure must be mounted before `Workflow` is instantiated:
+
+```html
+
+```
+
+This CSS example constrains the viewport height and enables vertical scrolling:
+
+```css
+#viewport { height: 300px; overflow-y: auto; }
+```
+
+The content element is passed to `Workflow`:
+
+```js
+const element = document.getElementById('content');
+const workflow = new Workflow({ element, ... });
+```
+
+By default, the Scroller uses the content element's parent as its viewport; [Configuration](configuration.md#viewport-and-horizontal-scrolling) explains how to select a different scroll target.
+
+The padding elements remain mounted as rows come and go; the Scroller sizes them to represent virtual space. Rows must be measurable. By default, their size comes from `getBoundingClientRect()`, which excludes margins; unaccounted margins or gaps distort the virtual geometry. Row measurement can be customized through [Routines](routines.md#geometry-and-padding-sizes).
+
+## The `run(items)` callback
+
+Implementing `run(items)` is a central integration requirement for the consumer. The callback must turn the supplied items into DOM rows that correctly represent the current buffer. This remains the consumer's responsibility whether rendering is performed directly or through a framework.
+
+The Scroller calls `run(items)` whenever it updates the item buffer—for example, after fetching data, clipping distant items, or applying an Adapter mutation. The argument is the complete buffer, including items outside the visible viewport, not just newly fetched data. The content element must contain one row per item, in buffer order, between the padding elements.
+
+Each buffer entry is an `Item` wrapping application data. Its rendering-relevant fields are:
+
+| Field | Rendering role |
+| --- | --- |
+| `data` | Application value supplied through the datasource's `get` or an Adapter operation and displayed in the row. |
+| `$index` | Current dataset position, used for row order and `data-sid`; Adapter mutations may change it. |
+| `uid` | Item identity, stable while that item is retained even if its index changes; suitable as a renderer key. A refetched or replaced item has a new `uid`. |
+| `element` | Associated DOM row; initially absent for a new item and available after association. |
+| `invisible` | True until a new row is revealed by the Scroller; it does not mean the row is outside the viewport. |
+| `get()` | Returns the shared Adapter-facing item container; its fields remain live, not a snapshot. |
+
+A minimal direct-DOM implementation of `run(items)` uses the content element shown above and assumes each item's data has a `text` field:
+
+```js
+const element = document.getElementById('content');
+const forwardPadding = element.querySelector('[data-padding-forward]');
+let previous = [];
+
+function run(items) {
+ for (const item of previous) {
+ if (!items.includes(item)) item.element?.remove();
+ }
+
+ let next = forwardPadding;
+ for (const item of [...items].reverse()) {
+ const row = (item.element ??= document.createElement('div'));
+
+ row.dataset.sid = String(item.$index);
+ row.style.position = item.invisible ? 'fixed' : '';
+ row.style.top = item.invisible ? '-99999px' : '';
+ row.textContent = `${item.$index}: ${item.data.text}`;
+
+ if (row.nextElementSibling !== next) element.insertBefore(row, next);
+ next = row;
+ }
+
+ previous = [...items];
+}
+```
+
+The example illustrates five rendering requirements:
+
+1. **Represent the whole buffer.** The committed DOM has one row per item, in buffer order, between the unchanged padding elements. The callback must not mutate `items` or core-managed fields such as `uid` and `$index`.
+2. **Remove and reuse rows.** The example uses `previous` to identify items that left the buffer and remove their rows. Retained items reuse their DOM nodes through `item.element`; those nodes can be updated or moved as needed instead of recreated. Any consumer-owned resources attached to removed rows must also be released.
+3. **Keep rows current.** Row content reflects `item.data`, and `data-sid` matches the current `$index`, including when an Adapter operation changes the index of a retained item.
+4. **Keep new rows measurable.** With default Routines, `item.invisible` rows are positioned off-screen and outside normal flow, not hidden with `display: none`.
+5. **Commit in time.** Renderer state must exist before `new Workflow(...)`, which calls `run([])` during construction. With default Routines, DOM changes must be synchronous; a returned Promise is not awaited.
+
+After `run` adds new rows, `Routines.render` schedules the Scroller's processing of them. When its callback runs, those rows must be in the DOM: the Scroller finds them by `data-sid`, restores normal positioning and measures them. A renderer with a later DOM commit needs a custom [render hook](routines.md#scheduling-and-cancellation) that waits for that commit. Later row-size changes require [Adapter `check()`](adapter-methods.md#check) after the DOM update; the Scroller does not observe them automatically.
diff --git a/docs/routines.md b/docs/routines.md
new file mode 100644
index 00000000..2e96b72f
--- /dev/null
+++ b/docs/routines.md
@@ -0,0 +1,150 @@
+# Custom Routines
+
+[← Documentation index](index.md)
+
+`Routines` is the scroller's DOM operations class, exported from `vscroll`. It handles element lookup, geometry measurement, scrolling and scheduling of internal work. The engine uses the built-in implementation by default. Custom Routines adapt these operations to a particular layout or rendering mechanism without changing the scrolling algorithm.
+
+## Overriding and integration
+
+A custom implementation extends `Routines` and overrides the relevant methods; the others retain their default behavior. The class is passed as the `Routines` parameter when constructing `Workflow`. The engine instantiates it with the content element and resolved datasource settings.
+
+The exported `IRoutines` interface describes the class contract. The following instance properties are available through `this` in overridden methods:
+
+| Property | Type | Meaning |
+| --- | --- | --- |
+| `element` | `HTMLElement` | Content element passed to `Workflow`, containing rows and padding elements. |
+| `viewport` | `HTMLElement` | Viewport element determined by `getViewportElement()`. |
+| `settings` | `IRoutines['settings']` | Reduced settings: `viewport` is the explicitly configured viewport or `null`; `horizontal` selects horizontal mode; `window` holds the value of `windowViewport`. |
+
+For example, the [table demo](../demo/table-demo.html) uses `tbody` as the content element and explicitly selects an outer scrollable container through `settings.viewportElement`. The `getOffset()` method determines the list's offset relative to the viewport. For this layout, the override sets it to the height of the preceding table header:
+
+```js
+import { Routines, Workflow } from 'vscroll';
+
+class TableRoutines extends Routines {
+ getOffset() {
+ return this.viewport.querySelector('thead')?.offsetHeight || 0;
+ }
+}
+
+new Workflow({ Routines: TableRoutines, ... });
+```
+
+The inherited constructor initializes `element` and reduced `settings`, assigns `viewport` from `getViewportElement()`, then calls `onInit(settings)`. Both hooks run before subclass fields are initialized; `this.viewport` is not yet available inside `getViewportElement()`. A custom constructor must forward its arguments to `super`; overriding it is usually unnecessary.
+
+## Hiding and revealing new rows
+
+New rows are initially created in the DOM outside normal flow while the padding elements still represent the previous list state. The engine then reveals the rows, measures them and adjusts padding sizes and scroll position. Initial hiding in `run(items)` must therefore match `makeElementVisible`, which restores the rows to the layout before measurement.
+
+Another example of Custom Routines replaces the default off-screen positioning with a CSS class that hides new rows using `display: none`. Below, `run` applies the class according to `item.invisible`, and `DisplayRoutines` removes it. [Rendering](rendering.md#the-runitems-callback) provides a complete `run` implementation; the version below is schematic.
+
+```css
+#content > .vscroll-pending { display: none !important; }
+```
+
+```js
+import { Routines, Workflow } from 'vscroll';
+
+class DisplayRoutines extends Routines {
+ makeElementVisible(element) {
+ this.checkElement(element);
+ element.classList.remove('vscroll-pending');
+ }
+}
+
+function run(items) {
+ ... // remove rows no longer in the buffer
+ for (const item of items) {
+ ... // create or reuse row
+ row.classList.toggle('vscroll-pending', item.invisible);
+ ... // update row content and DOM order
+ }
+ ...
+}
+
+new Workflow({ element, run, Routines: DisplayRoutines, ... });
+```
+
+While the class is applied, the row is excluded from layout and its DOM size is zero. The engine calls `makeElementVisible` before `getSize`, so measurement uses the revealed row. Removing the class preserves the original `display` defined by CSS or an inline style; `!important` also allows temporary hiding when an ordinary inline `display` is present. The default `hideElement` remains unchanged: it is used before removing rows, not for their initial preparation.
+
+## Scheduling and cancellation
+
+With asynchronous rendering, `run(items)` may return before the DOM is updated. `Routines.render(cb, { items })` determines when the engine continues processing rows: a custom implementation calls `cb` once the DOM is ready. The engine does not await a Promise returned by `run`.
+
+If the DOM is guaranteed to be ready by the next animation frame, the default `render` timer can be replaced with a frame callback:
+
+```js
+class FrameRoutines extends Routines {
+ render(cb) {
+ const id = requestAnimationFrame(cb);
+ return () => cancelAnimationFrame(id);
+ }
+}
+```
+
+An animation frame does not guarantee DOM readiness in every framework. If the DOM updates later, the framework's own render-completion signal is required.
+
+The scheduling methods `render` and `animate` share a contract:
+
+1. `cb` is called asynchronously, exactly once unless the work is cancelled.
+2. The method immediately returns a cancellation function. It prevents a pending `cb` call, never invokes `cb` itself, and is safe to call repeatedly.
+3. The engine uses this function when abandoning pending work or during disposal; a cancelled callback must not resume processing.
+
+Calls to `run` and `render` are not necessarily paired: clipping publishes the remaining buffer without processing new rows.
+
+The consumer releases any additional integration resources; `Routines.dispose()` is not called automatically.
+
+## Method reference
+
+The tables below list all methods of the built-in class and their default behavior. The signatures also use the exported `Direction` enum: `backward` means the top or left side, and `forward` the bottom or right side.
+
+### Initialization and element lookup
+
+| Method | Default behavior |
+| --- | --- |
+| `checkElement(element: HTMLElement): void` | Checks that an element is provided; throws if it is missing. |
+| `getViewportElement(): HTMLElement` | Returns `document.documentElement` in window mode; otherwise, the explicit `settings.viewport` or the parent of `element`. |
+| `onInit(settings): void` | Receives the full resolved settings object, unlike reduced `this.settings`. In window mode, sets `history.scrollRestoration = 'manual'` when supported; with `dismissOverflowAnchor`, sets the viewport's `overflowAnchor = 'none'`. Calling `super.onInit(settings)` preserves this behavior in an override. |
+| `findElementBySelector(element: HTMLElement, selector: string): HTMLElement \| null` | Returns the first `querySelector` match within the supplied element. |
+| `findPaddingElement(direction: Direction): HTMLElement \| null` | Finds a padding element in `element` with `[data-padding-backward]` or `[data-padding-forward]`. |
+| `findItemElement(id: string): HTMLElement \| null` | Finds a row in `element` with `[data-sid=""]`. The engine supplies the item's current index as a string. |
+| `findItemChildBySelector(id: string, selector: string): HTMLElement \| null` | Finds a row's descendant using `[data-sid=""] `. |
+
+Browser settings changed by `onInit` are not automatically restored when `Workflow` is disposed. Any required restoration belongs to the integration.
+
+### Geometry and padding sizes
+
+Sizes and coordinates are expressed in CSS pixels. The active dimension is height for vertical scrolling and width for horizontal scrolling. Custom geometry must keep row and content sizes, viewport edges, list offset and padding sizes consistent. For example, `getSizeStyle` and `setSizeStyle` must read and write sizes in the same way.
+
+| Method | Default behavior |
+| --- | --- |
+| `getElementParams(element: HTMLElement): DOMRect` | Returns the element's `getBoundingClientRect()`. |
+| `getWindowParams(): DOMRect` | Returns the window rectangle starting at `(0, 0)`, with dimensions `window.innerWidth` and `window.innerHeight`. |
+| `getSize(element: HTMLElement): number` | Returns the active dimension from `getElementParams(element)`, excluding margins. Used for both rows and the viewport. |
+| `getScrollerSize(): number` | Returns the content element's active dimension from `getElementParams(this.element)`, not its `scrollHeight` or `scrollWidth`. |
+| `getViewportSize(): number` | Returns the active dimension from `getWindowParams()` in window mode; otherwise, `getSize(viewport)`. |
+| `getSizeStyle(element: HTMLElement): number` | Reads inline `height` or `width` using `parseFloat`, returning `0` when absent. Does not read computed CSS. |
+| `setSizeStyle(element: HTMLElement, value: number): void` | Rounds the size, clamps it to zero or greater, and writes inline `height` or `width` in pixels. Used for padding elements. |
+| `getEdge(element: HTMLElement, direction: Direction): number` | Returns the corresponding edge from `getElementParams(element)`. |
+| `getViewportEdge(direction: Direction): number` | Returns the corresponding edge of the window rectangle in window mode; otherwise, `getEdge(viewport, direction)`. |
+| `getOffset(): number` | Returns the content element's `offsetTop` or `offsetLeft`, minus the viewport's corresponding offset. No subtraction is performed in window mode. |
+
+### Visibility and scrolling
+
+Revealing new rows must match their initial hiding in `run`: the default implementation expects off-screen positioning, not `display: none`.
+
+| Method | Default behavior |
+| --- | --- |
+| `makeElementVisible(element: HTMLElement): void` | Clears inline `left`, `top` and `position`, returning the row to normal flow. Does not clear `display`. |
+| `hideElement(element: HTMLElement): void` | Sets `display: none` before row removal. This is a separate operation, not the inverse of `makeElementVisible`. |
+| `getScrollPosition(): number` | Returns `pageYOffset` or `pageXOffset` in window mode; otherwise, the viewport's `scrollTop` or `scrollLeft`. |
+| `setScrollPosition(value: number): void` | Clamps the value to zero or greater and sets the position through `window.scrollTo` or the viewport's scroll property. Window scrolling preserves the other axis; browser limits still apply. |
+| `scrollTo(element: HTMLElement, argument?: boolean \| ScrollIntoViewOptions): void` | Calls `element.scrollIntoView(argument)` to scroll to a specific element. |
+
+### Scheduling and scroll listener
+
+| Method | Default behavior |
+| --- | --- |
+| `render(cb: () => void, params: { items: IAdapterItem[] }): () => void` | Calls `cb` through `setTimeout` with no explicit delay and returns a timer cancellation function. `params.items` contains the current batch's item containers, not the whole buffer; their DOM references may not yet exist. The default implementation ignores `params`. |
+| `animate(cb: () => void): () => void` | Schedules the scroll-position adjustment callback through `requestAnimationFrame` and returns a frame cancellation function. This is not animation of individual rows. |
+| `onScroll(handler: EventListener): () => void` | Attaches a `scroll` listener to the window or viewport. Returns a function to remove it, which the engine calls during `Workflow` disposal. |
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 00000000..f6574597
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,76 @@
+# Troubleshooting
+
+[← Documentation index](index.md)
+
+The scroller depends on data retrieval, rendering the buffer in the DOM and measuring its geometry. The table below connects common problems to these requirements; the diagnostic log shows the engine's actions and changes in list state.
+
+## Common problems
+
+| Symptom | Possible cause |
+| --- | --- |
+| Error during scroller construction | The exception message identifies an invalid argument or a missing padding element. `run` must declare one parameter, and `get` at least two. The contracts are described in [Workflow](workflow.md#constructor), [Datasource](datasource.md#asynchronous-delivery-and-errors) and [Rendering](rendering.md#required-dom-and-layout). |
+| `Can not associate item with element` | When new rows are processed, the DOM has not yet been updated or a row cannot be found by its current `data-sid`. See the [`run` contract and rendering timing](rendering.md#the-runitems-callback). |
+| Loading never finishes | An unsettled Promise, a missing callback invocation or an Observable completing without an array or an error leaves the `get` request pending. Returning an array directly is not supported. This is a data-delivery problem, not a datasource validation failure; see [supported `get` signatures](datasource.md#asynchronous-delivery-and-errors). |
+| Loading stops before the end of the dataset | A short or empty `get` response signals a dataset boundary, not a partial page or an error. See [data boundaries](datasource.md#data-ranges-and-boundaries). |
+| Data arrives, but the list or scroll range is missing | The viewport must be scrollable and have a constrained size, and rows must be measurable. `itemSize` provides an estimate, not a CSS size. See the [DOM geometry requirements](rendering.md#required-dom-and-layout). |
+| The list jumps when rows are added or removed | Recreating retained DOM nodes, incorrect row order or unaccounted margins and gaps disrupt position preservation and geometry. See the [rendering requirements](rendering.md#the-runitems-callback). |
+| Images or other content change row sizes later | Size changes are not tracked automatically. After the DOM update, [Adapter `check()`](adapter-methods.md#check) remeasures rows and corrects the geometry if needed. |
+| A changed setting has no effect | Settings are read during scroller construction; modifying the original object does not update a running instance. New configuration is supplied through `reset()`. See [applying settings](configuration.md#validation-and-applying-changes). |
+| An Adapter method succeeds without performing an operation | Before initialization or while paused, a call may complete with `success: true`, `immediate: true` and an explanation in `details`. See [calling methods](adapter-methods.md#calling-methods). |
+| Removed data reappears after scrolling | Adapter operations change the scroller's list, not the application's underlying data. Subsequent `get` responses must reflect these changes; see [data consistency](adapter-methods.md#data-consistency). |
+
+## Development log
+
+The diagnostic log connects data requests, rendering, clipping and viewport adjustments. It shows where a problem occurs and how the buffer and geometry change.
+
+### Obtaining a log
+
+Logging is enabled through the datasource's `devSettings.debug`. For example, deferred output can be configured as follows:
+
+```js
+import { makeDatasource } from 'vscroll';
+
+const Datasource = makeDatasource();
+const datasource = new Datasource({
+ get,
+ devSettings: { debug: true, immediateLog: false }
+});
+```
+
+Here, `get` is the application's data-retrieval function, and `datasource` is passed to `Workflow`. After the relevant actions, the recorded messages can be printed through the Adapter:
+
+```js
+datasource.adapter.showLog();
+```
+
+`showLog()` prints and clears the recorded log. With `immediateLog: true` (the default), messages appear in the console immediately; with `debug: false`, logging is disabled. Deferred messages remain in memory until printed, so leaving deferred logging enabled can increase memory usage.
+
+`logProcessRun: true` adds internal process-start events, while `logColor: false` disables color formatting for plain-text output. Timing and other options are described in [diagnostic settings](configuration.md#diagnostics).
+
+### Reading a log
+
+The log begins with the core and consumer versions, followed by settings with defaults applied. The `WF Cycle … STARTED/FINALIZED` markers delimit a Workflow cycle: work triggered by initialization, a relevant scroll event or an Adapter operation. Within it, `loop … start/done` marks inner loops—passes through data retrieval, rendering, clipping and viewport adjustments. Unnecessary steps are skipped; remaining work starts another loop.
+
+The `fetch interval` entries show range calculations, including intermediate ones. An actual request is marked `going to fetch …`, and its response `resolved … items (index, count)`. Multiple calculated ranges do not imply multiple `get` calls.
+
+The `stat` entries connect row processing to geometry: `pos` is the scroll position, `size` the content element's size along the scroll axis, `bwd_p` and `fwd_p` the padding element sizes, and `default` the estimated row size. `items` and `range` describe the count and index range of revealed buffer rows, including those outside the visible area.
+
+The following initial-load example from the [Dev-Log wiki](https://github.com/dhilt/vscroll/wiki/Dev-Log) contains one cycle and three inner loops.
+
+
+
+1. **The first loop** requests 10 items starting at index `1`. After rendering, measurements produce a row-size estimate of `20`; `items` becomes `10`, and `range` becomes `[1..10]`.
+2. **The second loop** loads 10 preceding items starting at index `-9`. The buffer expands to `[-9..10]`, and the scroll position is adjusted to `200`, preserving the original position of item `1` in the viewport.
+3. **The third loop** determines that no new data is needed: `fetch interval after Buffer flushing` reports `no`. There is no `get` request, and the cycle ends with `FINALIZED`.
+
+Multiple loops and requests within one cycle are not, by themselves, signs of a problem.
+
+The log is complemented by `workflow.errors`, which records engine failures with their process, message, time and inner loop. This list is available until `workflow.dispose()` and does not capture all JavaScript exceptions.
+
+## Reporting a problem
+
+Problems with the scroller can be reported through [GitHub issues](https://github.com/dhilt/vscroll/issues). A report must include a link to a minimal runnable demo that reproduces the issue, hosted on StackBlitz or another publicly accessible platform.
+
+The [VScroll test demo](https://stackblitz.com/edit/vscroll-1-8-4-test-demo) can be forked as a starting point.
+
+The report describes the steps to reproduce, expected behavior and actual behavior. Diagnostic logs and error messages can be attached when available.
diff --git a/docs/virtual-scrolling.md b/docs/virtual-scrolling.md
new file mode 100644
index 00000000..1d436a24
--- /dev/null
+++ b/docs/virtual-scrolling.md
@@ -0,0 +1,17 @@
+# Virtual scrolling model
+
+[← Documentation index](index.md)
+
+To the end user, VScroll presents one continuous scrollable list. It maintains an ordered buffer of items; the **consumer**—an application or framework integration—renders a DOM row for each. Dataset positions outside the buffer remain virtual, represented by empty space.
+
+ 
+
+The dark-blue region shows rows visible through the **viewport**, whose height $H_v$ is set by page layout. The light-blue **outlets** are rendered rows just outside it. Together, these rows represent the buffer in the DOM. `settings.padding` targets each outlet's size as a fraction of $H_v$ (default `0.5`); an outlet may be shorter near a dataset boundary.
+
+The white regions represent virtual rows. Empty **padding elements** before and after the buffer reserve their estimated space. Unlike outlets, these elements contain no rendered rows. See [Rendering](rendering.md) for the DOM structure.
+
+The items in this buffer originate with the [datasource](datasource.md) supplied to `Workflow`: it returns application values for requested index ranges. VScroll wraps each value in an item: `item.data` holds the value, while `item.$index` is its current dataset position—the number shown in the diagrams.
+
+The consumer supplies `run(items)` when constructing `Workflow`. During initialization and after buffer changes, VScroll calls it with the complete current item buffer, not just newly fetched items. The consumer uses this list to keep the corresponding DOM rows in sync. See [Rendering](rendering.md#the-runitems-callback) for the implementation contract.
+
+As the user scrolls, VScroll requests data for approaching positions, adds the resulting items to the buffer, clips distant items, and adjusts the padding elements. It measures rendered rows to refine its estimates of virtual space and may correct the scroll position to keep content in place. Setting [`settings.infinite = true`](configuration.md#settings) switches the scroller from virtual scrolling to infinite scrolling: automatic clipping stops and loaded rows accumulate. The dataset itself may still be finite.
diff --git a/docs/workflow.md b/docs/workflow.md
new file mode 100644
index 00000000..02eae55a
--- /dev/null
+++ b/docs/workflow.md
@@ -0,0 +1,53 @@
+# Workflow and lifecycle
+
+[← Documentation index](index.md)
+
+Constructing `Workflow` starts the virtual scroll engine for a mounted list. Prepare the content DOM and renderer first. See [Virtual scrolling model](virtual-scrolling.md) for how the datasource, item buffer and DOM fit together.
+
+## Constructor
+
+```ts
+import { Workflow, makeDatasource } from 'vscroll';
+
+const Datasource = makeDatasource();
+const datasource = new Datasource({ get, settings });
+
+const workflow = new Workflow({
+ consumer: { name: 'my-integration', version: '1.0.0' },
+ element: contentElement,
+ datasource,
+ run: items => render(items)
+});
+```
+
+Here `MyRecord`, `get`, `settings`, `contentElement` and `render` are supplied by the integration. The datasource can also be a plain object with `get`; the factory makes the [Adapter API](adapter.md) available before Workflow construction.
+
+| Parameter | Type | Contract |
+| --- | --- | --- |
+| `consumer` | `{ name: string; version: string }` | Static integration metadata used in diagnostics. |
+| `element` | `HTMLElement` | Mounted **content** element containing the padding elements, not the scrollable viewport. See [Rendering](rendering.md#required-dom-and-layout). |
+| `datasource` | `IDatasource` | Supplies indexed data and optional scrolling configuration. See [Datasource](datasource.md). |
+| `run` | `(items: Item[]) => void` | Consumer callback for rendering buffered items. It must declare one parameter; its return value is not awaited. See [Rendering](rendering.md#the-runitems-callback). |
+| `Routines` | Subclass of `Routines`, optional | Customizes DOM operations and render scheduling. See [Custom Routines](routines.md). |
+
+`run(items)` receives the complete current buffer of VScroll items. The integration uses it to make the DOM represent that buffer: one row per item, in order, reusing retained rows and removing obsolete ones. This is the core rendering contract; the [Rendering and DOM contract](rendering.md) explains its requirements with examples. A consumer can encapsulate both `run` and the `Workflow` lifecycle, so applications using it need not handle either directly.
+
+The engine calls `run(items)` whenever it assigns a new item buffer—for example, after fetching data, clipping distant items or applying an Adapter mutation. It is not called for every scroll event, nor is it limited to one call per cycle.
+
+The first call is `run([])` during construction, before the constructor returns, even if later initialization is delayed. Prepare the renderer beforehand; `run` must not depend on the `workflow` variable being assigned yet.
+
+The constructor can throw on invalid inputs. Returning from it does not mean the initial data has finished loading. Use the [Adapter API](adapter-methods.md#calling-methods) to observe initialization and wait for the first cycle to settle. See [Development settings](configuration.md#development-settings) for initialization delays.
+
+## Disposal and recreation
+
+Call `workflow.dispose()` once before removing the view. It detaches the scroll listener, cancels scheduled core work and detaches the Adapter. It does **not** abort datasource requests, cancel consumer-owned rendering, remove DOM nodes or release application subscriptions. The integration must clean up those resources; do not expect a final `run([])` call. If the constructed datasource will not be reused, call its `dispose()` afterward to release its factory bookkeeping.
+
+To recreate, dispose the old Workflow, reset the consumer's rendered-item state, leave or restore the two empty padding elements, then construct a new Workflow with the same datasource. Do not dispose that datasource between instances or attach it to two live workflows.
+
+Use [Adapter `reload`](adapter-methods.md#reload) to re-read data and [Adapter `reset`](adapter-methods.md#reset) to change datasource configuration without replacing the Workflow.
+
+## Diagnostics
+
+While the Workflow is alive, `isInitialized` and `disposed` describe its lifecycle; `cyclesDone` and `interruptionCount` count completed cycles and interruptions. `errors` records engine failures with `process`, `message`, `time` and `loop`, but does not capture arbitrary exceptions from application code. Read diagnostics before disposal: most instance fields are removed then.
+
+`cyclesDone$` notifies completed cycles before the final loading-state transition. It is useful for observation, not for waiting until idle; use `adapter.relax()` for that. Control a running scroller through the [Adapter](adapter-methods.md), not Workflow's internal process methods. See [Troubleshooting](troubleshooting.md) for failure diagnosis.