vlist 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.github.md +33 -15
  2. package/README.md +14 -12
  3. package/dist/constants.d.ts +28 -0
  4. package/dist/core/adapter.d.ts +92 -0
  5. package/dist/core/pipeline.d.ts +2 -0
  6. package/dist/core/runway.d.ts +82 -0
  7. package/dist/core/state.d.ts +6 -2
  8. package/dist/core/types.d.ts +31 -0
  9. package/dist/index.d.ts +7 -2
  10. package/dist/index.js +1 -1
  11. package/dist/internals.d.ts +2 -2
  12. package/dist/internals.js +1 -1
  13. package/dist/plugins/a11y/plugin.d.ts +10 -1
  14. package/dist/plugins/carousel/engine.d.ts +56 -0
  15. package/dist/plugins/carousel/index.d.ts +7 -0
  16. package/dist/plugins/carousel/plugin.d.ts +44 -0
  17. package/dist/plugins/carousel/presets.d.ts +18 -0
  18. package/dist/plugins/grid/renderer.d.ts +1 -4
  19. package/dist/plugins/groups/sticky.d.ts +1 -1
  20. package/dist/plugins/masonry/renderer.d.ts +1 -1
  21. package/dist/plugins/scale/index.d.ts +0 -3
  22. package/dist/plugins/scale/plugin.d.ts +1 -18
  23. package/dist/plugins/scrollbar/index.d.ts +0 -1
  24. package/dist/plugins/search/index.d.ts +8 -0
  25. package/dist/plugins/search/match.d.ts +34 -0
  26. package/dist/plugins/search/plugin.d.ts +84 -0
  27. package/dist/plugins/search/searchbar.d.ts +57 -0
  28. package/dist/plugins/selection/plugin.d.ts +7 -0
  29. package/dist/plugins/table/renderer.d.ts +2 -5
  30. package/dist/rendering/index.d.ts +1 -6
  31. package/dist/size.json +1 -1
  32. package/dist/types.d.ts +31 -9
  33. package/dist/vlist-search.css +1 -0
  34. package/package.json +2 -1
  35. package/dist/plugins/scrollbar/controller.d.ts +0 -121
  36. package/dist/rendering/measured.d.ts +0 -52
  37. package/dist/rendering/renderer.d.ts +0 -113
  38. package/dist/rendering/scale.d.ts +0 -121
  39. package/dist/rendering/scroll.d.ts +0 -23
  40. package/dist/rendering/viewport.d.ts +0 -139
package/README.github.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # vlist
2
2
 
3
- The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins — in 8.0 KB.
3
+ The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins — in 9.7 KB.
4
4
 
5
- **v2.2.0** — [Changelog](./CHANGELOG.md) · Table `fillWidth` (fills the container by default), async loading resilience (reload keeps placeholders, auto-retry with backoff), table placeholder skeletons.
5
+ **v2.4.0** — [Changelog](./CHANGELOG.md) · **New:** Bounded logical scroll model for 1M+ items (`scroll: { mode: "bounded" }`), `carousel()` plugin with infinite loop, snap, and focal scaling. `scale()` deprecated.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
8
8
  [![bundle size](https://img.shields.io/bundlephobia/minzip/vlist)](https://bundlephobia.com/package/vlist)
@@ -11,9 +11,9 @@ The virtual list library for every framework. Ultra efficient, batteries-include
11
11
 
12
12
  - **Accessible** — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
13
13
  - **Zero dependencies** — framework-agnostic core with tiny adapters for Vue, Svelte, Solid, React
14
- - **8.0 KB gzipped** — composable plugins with perfect tree-shaking
14
+ - **9.7 KB gzipped** — composable plugins with perfect tree-shaking
15
15
  - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
16
- - **Tree, grid, masonry, table, groups, data, selection, sortable, transition, scale** — all opt-in
16
+ - **Tree, grid, masonry, carousel, table, groups, data, selection, search, sortable, transition** — all opt-in
17
17
  - **Axis-neutral** — vertical and horizontal scrolling through a single code path, all plugins work in both orientations
18
18
 
19
19
  **18 interactive examples, docs & benchmarks → [vlist.io](https://vlist.io)**
@@ -94,20 +94,21 @@ const list = createVList({
94
94
 
95
95
  | Plugin | Size | Description |
96
96
  |--------|------|-------------|
97
- | **Base** | 8.0 KB | Virtualization, ARIA, keyboard nav, gap, padding |
97
+ | **Base** | 9.7 KB | Virtualization, ARIA, keyboard nav, gap, padding, bounded scroll (1M+ items) |
98
98
  | `data()` | +4.8 KB | Lazy loading with velocity-aware fetching |
99
- | `selection()` | +2.5 KB | Single/multiple selection with 2D keyboard nav |
100
- | `scale()` | +3.9 KB | 1M+ items via scroll compression |
101
- | `groups()` | +5.2 KB | Sticky/inline headers with grid + masonry + table + data integration |
99
+ | `selection()` | +2.7 KB | Single/multiple selection with 2D keyboard nav |
100
+ | `search()` | +3.2 KB | Search bar: filter/navigate modes, match highlighting |
101
+ | `groups()` | +5.3 KB | Sticky/inline headers with grid + masonry + table + data integration |
102
102
  | `autosize()` | +0.8 KB | Auto-measure items via ResizeObserver |
103
103
  | `scrollbar()` | +2.0 KB | Custom scrollbar UI |
104
- | `grid()` | +2.9 KB | 2D grid layout |
105
- | `masonry()` | +3.7 KB | Pinterest-style masonry with lane-aware keyboard nav |
106
- | `table()` | +6.2 KB | Data table with columns, resize, sort |
107
- | `tree()` | +5.3 KB | Collapsible tree with async loading and indent guides |
104
+ | `grid()` | +2.4 KB | 2D grid layout |
105
+ | `masonry()` | +4.0 KB | Pinterest-style masonry with lane-aware keyboard nav |
106
+ | `carousel()` | +2.3 KB | Paged horizontal carousel with snap and keyboard nav |
107
+ | `table()` | +5.8 KB | Data table with columns, resize, sort |
108
+ | `tree()` | +5.0 KB | Collapsible tree with async loading and indent guides |
108
109
  | `page()` | +0.8 KB | Window-level scrolling |
109
- | `sortable()` | +2.9 KB | Drag-and-drop reordering with auto-scroll |
110
- | `snapshots()` | +1.3 KB | Scroll position save/restore |
110
+ | `sortable()` | +3.0 KB | Drag-and-drop reordering with auto-scroll |
111
+ | `snapshots()` | +1.1 KB | Scroll position save/restore |
111
112
  | `transition()` | +1.8 KB | FLIP-based enter/exit animations for insert & remove |
112
113
 
113
114
  ## Examples
@@ -215,6 +216,24 @@ const list = createVList({
215
216
  ])
216
217
  ```
217
218
 
219
+ ### Search
220
+
221
+ ```typescript
222
+ import { createVList, search } from 'vlist'
223
+ import 'vlist/styles/search'
224
+
225
+ const list = createVList({
226
+ container: '#app',
227
+ items: people,
228
+ item: { height: 48, template: renderPerson },
229
+ }, [
230
+ // Zero-config: a search bar at the top, Ctrl/⌘+F to focus, type to filter,
231
+ // matches highlighted with <mark>. Use mode: 'navigate' to jump between
232
+ // matches instead of hiding non-matches.
233
+ search(),
234
+ ])
235
+ ```
236
+
218
237
  ## Accessibility
219
238
 
220
239
  Every vlist is accessible by default following the [WAI-ARIA listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
@@ -305,7 +324,6 @@ selection({ mode: 'single' | 'multiple', initial?: [...ids] })
305
324
  data({ adapter: { read }, loading?: { cancelThreshold? } })
306
325
  table({ columns, rowHeight, headerHeight?, resizable? })
307
326
  autosize() // auto-measure items (requires estimatedHeight)
308
- scale() // auto-activates at 16.7M px
309
327
  scrollbar({ autoHide?, autoHideDelay?, minThumbSize? })
310
328
  transition({ duration?: 200, insert?: timing, remove?: timing })
311
329
  sortable({ handle?: '.drag-handle' }) // drag-and-drop reordering
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # vlist
2
2
 
3
- The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins — in 8.0 KB.
3
+ The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins — in 9.7 KB.
4
4
 
5
- **v2.2.0** — [Changelog](https://github.com/floor/vlist/blob/main/CHANGELOG.md) · **New:** table `fillWidth` (fills the container by default), async loading resilience (reload keeps placeholders, auto-retry with backoff), and table placeholder skeletons.
5
+ **v2.4.0** — [Changelog](https://github.com/floor/vlist/blob/main/CHANGELOG.md) · **New:** Bounded logical scroll model for 1M+ items (`scroll: { mode: "bounded" }`), `carousel()` plugin with infinite loop, snap, and focal scaling. `scale()` deprecated.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
8
8
  [![bundle size](https://img.shields.io/bundlephobia/minzip/vlist)](https://bundlephobia.com/package/vlist)
@@ -11,7 +11,7 @@ The virtual list library for every framework. Ultra efficient, batteries-include
11
11
 
12
12
  - **Accessible** — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
13
13
  - **Zero dependencies** — framework-agnostic core, tiny adapters for Vue, Svelte, Solid, React
14
- - **8.0 KB gzipped** — composable plugins with perfect tree-shaking
14
+ - **9.7 KB gzipped** — composable plugins with perfect tree-shaking
15
15
  - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
16
16
  - **Axis-neutral** — vertical and horizontal scrolling through a single code path, all plugins work in both orientations
17
17
 
@@ -56,19 +56,21 @@ const list = createVList({ container: '#app', items, item: { height: 200, templa
56
56
 
57
57
  | Plugin | Size | Description |
58
58
  |--------|------|-------------|
59
- | **Base** | 8.0 KB | Virtualization, ARIA, keyboard nav, gap, padding |
59
+ | **Base** | 9.7 KB | Virtualization, ARIA, keyboard nav, gap, padding, bounded scroll (1M+ items) |
60
60
  | `data()` | +4.8 KB | Lazy loading with velocity-aware fetching |
61
- | `selection()` | +2.5 KB | Single/multiple selection with 2D keyboard nav |
62
- | `scale()` | +3.9 KB | 1M+ items via scroll compression |
63
- | `groups()` | +5.2 KB | Sticky/inline headers with grid + masonry + table + data integration |
61
+ | `selection()` | +2.7 KB | Single/multiple selection with 2D keyboard nav |
62
+ | `search()` | +3.2 KB | Search bar: filter/navigate modes, match highlighting |
63
+ | `groups()` | +5.3 KB | Sticky/inline headers with grid + masonry + table + data integration |
64
64
  | `autosize()` | +0.8 KB | Auto-measure items via ResizeObserver |
65
65
  | `scrollbar()` | +2.0 KB | Custom scrollbar UI |
66
- | `grid()` | +2.9 KB | 2D grid layout |
67
- | `masonry()` | +3.7 KB | Pinterest-style masonry with lane-aware keyboard nav |
68
- | `table()` | +6.2 KB | Data table with columns, resize, sort |
66
+ | `grid()` | +2.4 KB | 2D grid layout |
67
+ | `masonry()` | +4.0 KB | Pinterest-style masonry with lane-aware keyboard nav |
68
+ | `carousel()` | +2.3 KB | Paged horizontal carousel with snap and keyboard nav |
69
+ | `table()` | +5.8 KB | Data table with columns, resize, sort |
70
+ | `tree()` | +5.0 KB | Collapsible tree with async loading and indent guides |
69
71
  | `page()` | +0.8 KB | Window-level scrolling |
70
- | `sortable()` | +2.9 KB | Drag-and-drop reordering with auto-scroll |
71
- | `snapshots()` | +1.3 KB | Scroll position save/restore |
72
+ | `sortable()` | +3.0 KB | Drag-and-drop reordering with auto-scroll |
73
+ | `snapshots()` | +1.1 KB | Scroll position save/restore |
72
74
  | `transition()` | +1.8 KB | FLIP-based enter/exit animations for insert & remove |
73
75
 
74
76
  ## Framework Adapters
@@ -45,6 +45,34 @@ export declare const WHEEL_SENSITIVITY = 1;
45
45
  export declare const SCROLL_EASING: (t: number) => number;
46
46
  /** Default smooth scroll duration (ms) */
47
47
  export declare const SCROLL_DURATION = 300;
48
+ /**
49
+ * Runway size as a multiple of the viewport. The bounded content element is
50
+ * sized to `containerSize × this` (capped at total size), giving the native
51
+ * scrollbar room to move before a rebase shifts the logical origin.
52
+ */
53
+ export declare const BOUNDED_RUNWAY_FACTOR = 2;
54
+ /**
55
+ * Minimum runway multiple. The runway must exceed the viewport so native scroll
56
+ * and touch/trackpad momentum have room before a rebase; below this there would
57
+ * be little-to-no native scrollable range. User-supplied `scroll.runway` values
58
+ * are clamped up to this floor.
59
+ *
60
+ * 1.5 keeps a half-viewport of native scroll travel — enough for the native
61
+ * scrollbar and most touch gestures. The wheel/trackpad path is already fully
62
+ * synthetic (driven in logical space), so it is unaffected by the runway size;
63
+ * a small runway only risks interrupting native touch momentum at the edges.
64
+ */
65
+ export declare const BOUNDED_RUNWAY_MIN = 1.5;
66
+ /**
67
+ * Lower rebase trigger: when native scrollTop drops below this fraction of the
68
+ * runway (and we are not already at the logical start), shift the origin back.
69
+ */
70
+ export declare const BOUNDED_REBASE_LOW = 0.25;
71
+ /**
72
+ * Upper rebase trigger: when native scrollTop rises above this fraction of the
73
+ * runway (and we are not already at the logical end), shift the origin forward.
74
+ */
75
+ export declare const BOUNDED_REBASE_HIGH = 0.75;
48
76
  /** Default auto-hide behavior */
49
77
  export declare const SCROLLBAR_AUTO_HIDE = true;
50
78
  /** Default auto-hide delay in milliseconds */
@@ -0,0 +1,92 @@
1
+ /**
2
+ * vlist — Logical Scroll Model (RFC-012)
3
+ *
4
+ * The scroll coordinate is logical, not a physical pixel offset into a giant
5
+ * container: `{ index, offsetPx }` — the top visible item plus the pixel offset
6
+ * within it. This is stable under variable and autosized items, because the
7
+ * pixel offset stays physically meaningful when an item's measured size changes
8
+ * (unlike a fraction, which silently shifts the visual position).
9
+ *
10
+ * The pixel equivalent (`index * itemSize + offsetPx` generalised over the size
11
+ * cache) is derived only at the public API boundary, so existing consumers of
12
+ * `getScrollPosition()` and scroll events keep seeing pixel values (RFC-012 G4).
13
+ *
14
+ * This module is pure: it owns no DOM and no engine state. It is the translation
15
+ * layer the `ScrollAdapter` and the render pipeline build on.
16
+ */
17
+ import type { SizeCache } from "../rendering/sizes";
18
+ /**
19
+ * Logical scroll position: the top visible data item plus the pixel offset
20
+ * scrolled past its leading edge.
21
+ *
22
+ * Invariant after {@link normalizeLogical}: `0 <= offsetPx < size(index)`
23
+ * (except at the list end, where `offsetPx` may exceed the final item to
24
+ * represent overscroll clamping).
25
+ */
26
+ export interface LogicalScrollPosition {
27
+ /** Index of the top visible item (integer, clamped to `[0, total - 1]`). */
28
+ index: number;
29
+ /** Pixel offset scrolled past the leading edge of `index`. */
30
+ offsetPx: number;
31
+ }
32
+ /**
33
+ * The migration boundary between the logical scroll model and the rest of the
34
+ * engine and its plugins (RFC-012). Plugins read and write scroll position
35
+ * through this adapter rather than touching `engineState.scrollPosition` or raw
36
+ * DOM `scrollTop` / `scrollLeft` directly.
37
+ *
38
+ * It exposes both the logical primitive and a pixel-equivalent view so existing
39
+ * pixel-based code paths keep working during migration.
40
+ */
41
+ export interface ScrollAdapter {
42
+ /** Current logical position, normalized. */
43
+ getLogical(): LogicalScrollPosition;
44
+ /** Move to a logical position. Out-of-range input is clamped. */
45
+ setLogical(pos: LogicalScrollPosition): void;
46
+ /** Pixel-equivalent of the current position (for public API / events). */
47
+ getPixelEquivalent(): number;
48
+ /** Move to a pixel-equivalent position. Out-of-range input is clamped. */
49
+ setPixelEquivalent(px: number): void;
50
+ /** Maximum scrollable pixel-equivalent (`totalSize - containerSize`, >= 0). */
51
+ getMaxPixelEquivalent(): number;
52
+ /** Advance the position by a pixel delta (used by native/wheel input). */
53
+ scrollByPx(delta: number): void;
54
+ }
55
+ /** Dependencies an adapter needs to translate and apply scroll positions. */
56
+ export interface ScrollAdapterConfig {
57
+ /** Source of item offsets and sizes. */
58
+ readonly sizeCache: SizeCache;
59
+ /** Read the current pixel-equivalent position from the active scroll source. */
60
+ readonly getPixel: () => number;
61
+ /** Write a (clamped) pixel-equivalent position to the active scroll source. */
62
+ readonly setPixel: (px: number) => void;
63
+ /** Current viewport size along the main axis. */
64
+ readonly getContainerSize: () => number;
65
+ }
66
+ /**
67
+ * Convert a pixel offset into a logical position. The returned position is
68
+ * normalized: `index` is clamped to `[0, total - 1]` and `offsetPx` is the
69
+ * remainder past that item's leading edge.
70
+ */
71
+ export declare function pixelToLogical(sizeCache: SizeCache, pixel: number): LogicalScrollPosition;
72
+ /**
73
+ * Convert a logical position into a pixel offset. Input is normalized first,
74
+ * so callers may pass an `offsetPx` that overflows its item.
75
+ */
76
+ export declare function logicalToPixel(sizeCache: SizeCache, pos: LogicalScrollPosition): number;
77
+ /**
78
+ * Canonicalize a logical position so that `0 <= offsetPx < size(index)` where
79
+ * possible, carrying overflow/underflow across item boundaries. `index` is
80
+ * clamped to `[0, total - 1]`. At the list end, any remaining `offsetPx` is
81
+ * preserved (it represents scrolling past the start of the final item).
82
+ */
83
+ export declare function normalizeLogical(sizeCache: SizeCache, pos: LogicalScrollPosition): LogicalScrollPosition;
84
+ /**
85
+ * Create a {@link ScrollAdapter} over an existing pixel-based scroll source.
86
+ *
87
+ * During migration the underlying source is still the native viewport scroll;
88
+ * later phases can swap `getPixel` / `setPixel` for a bounded-proxy or
89
+ * interception input strategy without changing any plugin that uses the adapter.
90
+ */
91
+ export declare function createScrollAdapter(config: ScrollAdapterConfig): ScrollAdapter;
92
+ //# sourceMappingURL=adapter.d.ts.map
@@ -33,6 +33,8 @@ export interface RenderConfig {
33
33
  readonly crossEndVal: string;
34
34
  readonly oddClass: string;
35
35
  readonly emitter: Emitter<VListEvents> | null;
36
+ indexMap: ((renderIndex: number) => number) | null;
37
+ ariaTotalFn: (() => number) | null;
36
38
  }
37
39
  export declare function createRenderConfig(classPrefix: string, isX: boolean, startPadding: number, crossPadStart: number, crossPadEnd: number, oddClass: string, gap: number, emitter?: Emitter<VListEvents> | null): RenderConfig;
38
40
  /**
@@ -0,0 +1,82 @@
1
+ /**
2
+ * vlist — Bounded Logical Scroll Handler (RFC-012)
3
+ *
4
+ * An alternative to the native scroll handler ({@link createScrollHandler}) that
5
+ * decouples the native scrollable size from the virtual content size.
6
+ *
7
+ * The content element is sized to a bounded *runway* (a multiple of the
8
+ * viewport, capped at the real virtual size) instead of `totalItems × itemSize`.
9
+ * This sidesteps the browser's ~16.7M px element-size limit without the scale
10
+ * plugin's compression — no compression ratio leaks into offsets or hit-testing.
11
+ *
12
+ * Coordinates:
13
+ * logical = baseOffset + scrollTop (absolute virtual pixel position)
14
+ *
15
+ * - `state.scrollPosition` always holds the *logical* (absolute) position, so
16
+ * the render pipeline's `indexAtOffset(scrollPosition)` keeps working over
17
+ * the full size cache, and the public pixel-equivalent surface is unchanged.
18
+ * - `state.baseOffset` is the virtual offset that maps to native scrollTop=0.
19
+ * The pipeline renders each item at `getOffset(index) - baseOffset`, so items
20
+ * land inside the bounded runway. baseOffset is 0 for small lists (native).
21
+ *
22
+ * Rebasing: as native scrollTop approaches a runway edge we shift baseOffset and
23
+ * scrollTop together by the same delta, preserving `logical`. The synthetic
24
+ * scroll event this triggers reads the same logical position and is dropped by
25
+ * the "logical unchanged" guard — no suppress flag needed. The repositioning of
26
+ * items (their transforms depend on baseOffset) is committed by the explicit
27
+ * render that the rebasing scroll frame performs.
28
+ */
29
+ import type { EngineState } from "./state";
30
+ import type { ScrollHandler } from "./scroll";
31
+ export interface BoundedScrollHandler extends ScrollHandler {
32
+ /** Move to an absolute logical (virtual pixel) position; renders + schedules idle. */
33
+ setLogical(logicalPx: number): void;
34
+ /** Current absolute logical position (== state.scrollPosition). */
35
+ getLogical(): number;
36
+ /** Maximum scrollable logical position (`virtualTotal - containerSize`, >= 0). */
37
+ getMaxLogical(): number;
38
+ /** Recompute runway/derived values and resize the content element. */
39
+ refresh(totalSize: number): void;
40
+ }
41
+ /**
42
+ * Infinite-loop (wrap) configuration for the bounded handler (carousel, RFC-011).
43
+ *
44
+ * In wrap mode the logical position is never clamped to a maximum: instead it is
45
+ * periodically folded back toward {@link home} by whole laps once it drifts more
46
+ * than {@link thresholdLaps} laps away. Because the consumer maps virtual indices
47
+ * to real items via `index % realTotal`, shifting the logical position (and
48
+ * baseOffset) by a whole lap leaves the rendered items and their on-screen
49
+ * positions unchanged — the loop is seamless. The content element is sized to the
50
+ * runway exactly as in non-wrap mode, so large item counts never blow the
51
+ * browser's element-size limit.
52
+ */
53
+ export interface WrapConfig {
54
+ /** Current lap period in virtual px (`realTotal × stepSize`). */
55
+ readonly lapSize: () => number;
56
+ /** Logical position to fold back toward (e.g. the middle cycle). */
57
+ readonly home: () => number;
58
+ /** Fold the logical position back toward `home` once it drifts this many laps away. */
59
+ readonly thresholdLaps: number;
60
+ }
61
+ export interface BoundedScrollConfig {
62
+ readonly state: EngineState;
63
+ readonly viewport: HTMLElement;
64
+ readonly content: HTMLElement;
65
+ readonly isX: boolean;
66
+ readonly wheelEnabled: boolean;
67
+ readonly idleTimeout: number;
68
+ readonly scrollTarget?: EventTarget;
69
+ /** Main-axis padding folded into the virtual total (matches native content sizing). */
70
+ readonly mainAxisPadding: number;
71
+ /** Runway size as a multiple of the viewport (defaults to BOUNDED_RUNWAY_FACTOR). */
72
+ readonly runwayFactor?: number;
73
+ /** Infinite-loop config (carousel). When set, the logical position wraps by
74
+ * whole laps toward `home` instead of clamping to a maximum. */
75
+ readonly wrap?: WrapConfig;
76
+ /** Called synchronously per frame — triggers the 2-phase pipeline. */
77
+ readonly onFrame: () => void;
78
+ /** Called when scrolling becomes idle. */
79
+ readonly onIdle: () => void;
80
+ }
81
+ export declare function createBoundedScrollHandler(config: BoundedScrollConfig): BoundedScrollHandler;
82
+ //# sourceMappingURL=runway.d.ts.map
@@ -24,6 +24,12 @@ export interface EngineState {
24
24
  prevScrollPosition: number;
25
25
  /** 1 = forward, -1 = backward, 0 = idle */
26
26
  scrollDirection: number;
27
+ /**
28
+ * Bounded logical-scroll origin (RFC-012). The virtual offset that maps to
29
+ * native scrollTop=0. Items render at `getOffset(index) - baseOffset`.
30
+ * Defaults to 0, which makes native mode byte-identical (`offset - 0`).
31
+ */
32
+ baseOffset: number;
27
33
  containerSize: number;
28
34
  crossSize: number;
29
35
  totalItems: number;
@@ -32,8 +38,6 @@ export interface EngineState {
32
38
  renderPending: boolean;
33
39
  initialized: boolean;
34
40
  destroyed: boolean;
35
- isCompressed: boolean;
36
- compressionRatio: number;
37
41
  prevAriaTotal: number;
38
42
  /**
39
43
  * Resize buffers when container changes. Cold path only.
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { VListItem, ItemTemplate, ItemState } from "../types";
8
8
  import type { SizeCache } from "./sizes";
9
+ import type { ScrollAdapter } from "./adapter";
9
10
  import type { EngineState } from "./state";
10
11
  import type { Emitter } from "../events";
11
12
  /** Phase 1 hook — mutates EngineState in place, zero allocation */
@@ -60,6 +61,13 @@ export interface ElementPool {
60
61
  export interface PluginContext<T extends VListItem = VListItem> {
61
62
  readonly dom: DOMStructure;
62
63
  readonly sizeCache: SizeCache;
64
+ /**
65
+ * Logical scroll model boundary (RFC-012). Plugins read and write scroll
66
+ * position through this adapter rather than touching `getState().scrollPosition`
67
+ * or raw `scrollTop`/`scrollLeft`. Its pixel-equivalent view keeps existing
68
+ * pixel-based code paths working during the migration.
69
+ */
70
+ readonly scroll: ScrollAdapter;
63
71
  readonly pool: ElementPool;
64
72
  readonly config: ResolvedConfig;
65
73
  readonly emitter: Emitter<import("../types").VListEvents<T>>;
@@ -72,7 +80,10 @@ export interface PluginContext<T extends VListItem = VListItem> {
72
80
  enableListboxRole(): void;
73
81
  setSizeConfig(config: number | ((index: number) => number)): void;
74
82
  setScrollFns(get: () => number, set: (pos: number) => void): void;
83
+ /** Request the bounded scroll handler in infinite-loop (wrap) mode (carousel). */
84
+ setBoundedWrap(config: import("./runway").WrapConfig): void;
75
85
  setVirtualTotalFn(fn: () => number): void;
86
+ setIndexMapFn(fn: (renderIndex: number) => number): void;
76
87
  getItems(): readonly T[];
77
88
  getItem(index: number): T | undefined;
78
89
  getState(): EngineState;
@@ -87,6 +98,8 @@ export interface PluginContext<T extends VListItem = VListItem> {
87
98
  readonly rawSizeSpec: number | ((index: number, ...args: unknown[]) => number);
88
99
  scrollTo(position: number): void;
89
100
  smoothScrollTo(target: number | (() => number), duration: number, easing?: (t: number) => number, onComplete?: () => void): void;
101
+ /** Cancel any in-flight smooth-scroll animation on the active handler. */
102
+ cancelScroll(): void;
90
103
  disableDefaultScroll(): void;
91
104
  disableDefaultResize(): void;
92
105
  setScrollTarget(target: EventTarget): void;
@@ -113,6 +126,7 @@ export interface PluginContext<T extends VListItem = VListItem> {
113
126
  lr: number;
114
127
  scrollIndex: ((itemIndex: number) => number) | null;
115
128
  navigate: ((currentIndex: number, key: string, total: number) => number) | null;
129
+ total: (() => number) | null;
116
130
  };
117
131
  }
118
132
  export interface VListPlugin<T extends VListItem = VListItem> {
@@ -180,6 +194,23 @@ export interface CreateVListConfig<T extends VListItem = VListItem> {
180
194
  scrollbar?: "none";
181
195
  gutter?: "auto" | "stable";
182
196
  idleTimeout?: number;
197
+ /**
198
+ * Scroll model (RFC-012). `"native"` (default) sizes the content element to
199
+ * the full virtual size — simple, but hits the browser's ~16.7M px limit.
200
+ * `"bounded"` sizes the content to a viewport-multiple runway and rebases a
201
+ * logical origin near the edges, supporting unbounded item counts without
202
+ * compressing the scroll space.
203
+ */
204
+ mode?: "native" | "bounded";
205
+ /**
206
+ * Runway size as a multiple of the viewport, used only with
207
+ * `mode: "bounded"` (default 2). The bounded content element is sized to
208
+ * `viewport × runway` (capped at the real virtual size). Larger values mean
209
+ * more native-scroll headroom and less frequent rebasing, at the cost of a
210
+ * bigger content element. Clamped up to a minimum of 1.5 so native scroll and
211
+ * touch momentum always have some room.
212
+ */
213
+ runway?: number;
183
214
  };
184
215
  /** Defer initial render to the next animation frame. */
185
216
  defer?: boolean;
package/dist/index.d.ts CHANGED
@@ -9,8 +9,6 @@
9
9
  * @packageDocumentation
10
10
  */
11
11
  export { createVList } from "./core/create";
12
- export { scale } from "./plugins/scale";
13
- export type { ScalePluginConfig } from "./plugins/scale";
14
12
  export { scrollbar } from "./plugins/scrollbar";
15
13
  export type { ScrollbarPluginConfig } from "./plugins/scrollbar";
16
14
  export { grid } from "./plugins/grid";
@@ -37,6 +35,13 @@ export { sortable } from "./plugins/sortable";
37
35
  export type { SortablePluginConfig } from "./plugins/sortable";
38
36
  export { tree } from "./plugins/tree";
39
37
  export type { TreePluginConfig, FlatNode } from "./plugins/tree";
38
+ export { search, DEFAULT_SEARCH_TEXT } from "./plugins/search";
39
+ export type { SearchPluginConfig, SearchText } from "./plugins/search";
40
+ export { carousel } from "./plugins/carousel";
41
+ export type { CarouselPluginConfig, CarouselVariant, CarouselDirection, CarouselState } from "./plugins/carousel";
42
+ /** @deprecated Use `scroll: { mode: "bounded" }` instead. Will be removed in vlist 3.0. */
43
+ export { scale } from "./plugins/scale";
44
+ export type { ScalePluginConfig } from "./plugins/scale";
40
45
  export { createStats } from "./utils/stats";
41
46
  export type { Stats, StatsConfig, StatsState } from "./utils/stats";
42
47
  export { rebuild } from "./utils/rebuild";