@ceriousdevtech/vue-cerious-scroll 1.0.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to vue-cerious-scroll will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.1] - 2026-06-01
9
+
10
+ ### Changed
11
+ - Verified compatibility with `@ceriousdevtech/cerious-scroll` 1.0.2
12
+
13
+ ### Dependencies
14
+ - Peer dependency `@ceriousdevtech/cerious-scroll` tested against `^1.0.2` (range `^1.0.1` already satisfies this)
15
+
16
+ ---
17
+
18
+ ## [1.0.0] - 2026-02-02
19
+
20
+ ### Added
21
+ - Initial release of `@ceriousdevtech/vue-cerious-scroll`
22
+ - `<CeriousScroll>` component — drop-in virtual scroll list for Vue 3.3+
23
+ - `useCeriousScroll` composable for headless usage with custom container elements
24
+ - Synchronous `render()` rendering for each row — rows run with the app's `appContext` so globally registered components, directives, and plugins work normally
25
+ - Synchronous height measurement (no estimated heights, no correction passes)
26
+ - Full TypeScript support with exported prop and composable types
27
+ - Peer dependency on `@ceriousdevtech/cerious-scroll` core engine
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024–2026 Cerious DevTech LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @ceriousdevtech/vue-cerious-scroll
2
+
3
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
+ [![Live Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://ceriousdevtech.github.io/vue-cerious-scroll/)
5
+
6
+ **Vue 3 bindings for [Cerious Scroll™](https://www.npmjs.com/package/@ceriousdevtech/cerious-scroll)** — high-performance virtual scrolling with **O(1) memory**, consistent **60 FPS+**, and **native variable-height support with no height estimation**.
7
+
8
+ Rows are rendered into the engine's own measured containers via Vue's synchronous `render()`, so every row's real height is measured (never estimated) — exactly the guarantee that makes CeriousScroll precise. Rows are rendered with your app's `appContext`, so **globally registered components, directives, and installed plugins work normally** inside each row.
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @ceriousdevtech/vue-cerious-scroll @ceriousdevtech/cerious-scroll
16
+ ```
17
+
18
+ `vue` (>= 3.3) is a peer dependency.
19
+
20
+ ---
21
+
22
+ ## Demo
23
+
24
+ **[Live demo →](https://ceriousdevtech.github.io/vue-cerious-scroll/)** — 100,000 rows, fixed/variable-height toggle, imperative jump-to-row, and live viewport stats.
25
+
26
+ To run locally:
27
+
28
+ ```bash
29
+ npm install
30
+ npm run demo # dev server with HMR
31
+ npm run demo:build # production build to demo/dist
32
+ ```
33
+
34
+ The demo imports the wrapper by its package name, aliased to the library source,
35
+ so edits to `src/` are reflected live.
36
+
37
+ ---
38
+
39
+ ## Quick start (component)
40
+
41
+ Give the container a height; provide `items` and an `#item` scoped slot.
42
+
43
+ ```vue
44
+ <script setup lang="ts">
45
+ import { CeriousScroll } from '@ceriousdevtech/vue-cerious-scroll';
46
+
47
+ const items = Array.from({ length: 1_000_000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
48
+ </script>
49
+
50
+ <template>
51
+ <CeriousScroll :items="items" :style="{ height: '480px' }">
52
+ <template #item="{ item, index }">
53
+ <div class="row">{{ index }} — {{ item.name }}</div>
54
+ </template>
55
+ </CeriousScroll>
56
+ </template>
57
+ ```
58
+
59
+ Variable heights need no configuration — just render rows of whatever height; the
60
+ engine measures each one.
61
+
62
+ ### Without a full array (huge / sparse data)
63
+
64
+ ```vue
65
+ <CeriousScroll
66
+ :total-elements="100_000_000"
67
+ :get-item="(index) => loadRow(index)"
68
+ :style="{ height: '600px' }"
69
+ >
70
+ <template #item="{ item, index }">
71
+ <Row :data="item" :index="index" />
72
+ </template>
73
+ </CeriousScroll>
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Composable
79
+
80
+ `useCeriousScroll` gives you full control. Attach `containerRef` to your scroll
81
+ element; the composable renders the rows imperatively into the engine's measured
82
+ containers.
83
+
84
+ ```vue
85
+ <script setup lang="ts">
86
+ import { h } from 'vue';
87
+ import { useCeriousScroll } from '@ceriousdevtech/vue-cerious-scroll';
88
+
89
+ const { containerRef } = useCeriousScroll({
90
+ items,
91
+ renderItem: (item, index) => h('div', { class: 'row' }, `${index} — ${item.name}`),
92
+ });
93
+ </script>
94
+
95
+ <template>
96
+ <div ref="containerRef" style="height: 480px; position: relative; overflow: hidden" />
97
+ </template>
98
+ ```
99
+
100
+ > `renderItem` returns a Vue `VNodeChild` (use `h(...)`, or render JSX/TSX).
101
+
102
+ ---
103
+
104
+ ## Component props
105
+
106
+ | Prop | Type | Description |
107
+ | --- | --- | --- |
108
+ | `items` | `readonly TItem[]` | Optional data array. `totalElements` defaults to `items.length`. |
109
+ | `totalElements` | `number` | Total item count. Required if `items` is omitted. |
110
+ | `getItem` | `(index) => TItem` | Lazy item getter for large/sparse datasets. |
111
+ | `renderItem` | `(item, index) => VNodeChild` | Render prop alternative to the `#item` scoped slot. |
112
+ | `options` | `CeriousScrollOptions` | Engine options (keyboard/touch/wheel/scrollbar/etc.). Read once at creation. |
113
+ | `autoRender` | `boolean` | Re-render on scroll/resize/data changes. Default `true`. |
114
+
115
+ The row is provided by the **`#item` scoped slot** (`{ item, index }`) or the
116
+ `render-item` prop. Apply `class` / `style` directly to the component — they fall
117
+ through onto the scroll container (set a height!).
118
+
119
+ ### Events
120
+
121
+ | Event | Payload | Description |
122
+ | --- | --- | --- |
123
+ | `viewport-change` | `CeriousViewportChangeDetail` | Normalized viewport-change (wheel/touch/keyboard/scrollbar). |
124
+ | `measured-viewport` | `MeasuredViewportRange` | Measured range after each render pass. |
125
+ | `ready` | `CeriousScrollEngine` | The underlying engine instance, once ready. |
126
+
127
+ ### Imperative API (via template `ref`)
128
+
129
+ ```ts
130
+ const scroll = ref<InstanceType<typeof CeriousScroll> | null>(null);
131
+ // scroll.value?.jumpToElement(500);
132
+ // scroll.value?.scrollToPercentage(50);
133
+ // scroll.value?.reset();
134
+ // scroll.value?.render();
135
+ // scroll.value?.recalculate(); // drop cached heights + re-measure (see Notes)
136
+ // scroll.value?.scroller; // the raw engine
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Notes
142
+
143
+ - **No height estimation.** Rows are committed with Vue's synchronous `render()`
144
+ so the engine measures real `offsetHeight`. Later size changes are picked up by
145
+ the engine's built-in `ResizeObserver`.
146
+ - **`options` are read at creation.** Changing `options` after mount has no
147
+ effect; remount (e.g. with a `:key`) to apply new engine options.
148
+ - **Changing the item count** recreates the engine internally (scroll position
149
+ is preserved). Mutating items without changing the count just re-renders the
150
+ content in place (cheap; Vue patches each row, so focus/selection survive) — it
151
+ does **not** discard cached heights, so editable grids that produce a new
152
+ `items` array on every edit don't trigger a full viewport re-measure.
153
+ - **If every rendered row's height changes at once** (e.g. a density/layout
154
+ switch) the cached heights become stale and rows can misalign until the next
155
+ scroll. Call `recalculate()` (on the template `ref`, or from the composable
156
+ result) right after the change to drop the height cache and re-measure. Don't
157
+ call it on routine edits — a single cell edit keeps its row's size, and the
158
+ engine's built-in `ResizeObserver` picks up any incidental resize on its own.
159
+
160
+ ---
161
+
162
+ ## License
163
+
164
+ Licensed by **Cerious DevTech LLC** under the **MIT License** (see `LICENSE-MIT`).
165
+
166
+ 📧 info@ceriousdevtech.com
@@ -0,0 +1,102 @@
1
+ import { PropType, VNodeChild } from 'vue';
2
+ import { CeriousScrollOptions, MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
3
+ import { CeriousViewportChangeDetail } from './viewport-change';
4
+ /**
5
+ * High-performance virtual scroll list. Provide `items` (or `total-elements` +
6
+ * `get-item`) and either an `#item` scoped slot or a `render-item` prop; give
7
+ * the container a height (e.g. `:style="{ height: '400px' }"`).
8
+ *
9
+ * Imperative methods (`render`, `jumpToElement`, `scrollToPercentage`, `reset`,
10
+ * `recalculate`) and the underlying `scroller` are available via a template ref.
11
+ *
12
+ * ```vue
13
+ * <CeriousScroll :items="items" :style="{ height: '400px' }">
14
+ * <template #item="{ item, index }">
15
+ * <div class="row">{{ index }} — {{ item.name }}</div>
16
+ * </template>
17
+ * </CeriousScroll>
18
+ * ```
19
+ */
20
+ export declare const CeriousScroll: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
21
+ /** Total item count. Falls back to `items.length` when omitted. */
22
+ totalElements: {
23
+ type: PropType<number | null>;
24
+ default: null;
25
+ };
26
+ /** Optional items array (passed to the row template as `item`). */
27
+ items: {
28
+ type: PropType<readonly unknown[] | null>;
29
+ default: null;
30
+ };
31
+ /** Optional lazy getter for very large/sparse datasets (alternative to `items`). */
32
+ getItem: {
33
+ type: PropType<(index: number) => unknown>;
34
+ default: undefined;
35
+ };
36
+ /** Render prop alternative to the `#item` scoped slot. */
37
+ renderItem: {
38
+ type: PropType<(item: unknown, index: number) => VNodeChild>;
39
+ default: undefined;
40
+ };
41
+ /** Options forwarded to `new CeriousScroll(...)` (read once, at creation). */
42
+ options: {
43
+ type: PropType<CeriousScrollOptions>;
44
+ default: undefined;
45
+ };
46
+ /** Automatically render after scroll/resize/data changes. Default: `true`. */
47
+ autoRender: {
48
+ type: BooleanConstructor;
49
+ default: boolean;
50
+ };
51
+ }>, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
52
+ [key: string]: any;
53
+ }>, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
54
+ /** Normalized viewport-change payload (wheel/touch/keyboard/scrollbar). */
55
+ 'viewport-change': (_detail: CeriousViewportChangeDetail) => true;
56
+ /** Measured range after each render pass. */
57
+ 'measured-viewport': (_range: MeasuredViewportRange) => true;
58
+ /** Emitted once the underlying engine instance is ready (and after recreation). */
59
+ ready: (_scroller: unknown) => true;
60
+ }, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
61
+ /** Total item count. Falls back to `items.length` when omitted. */
62
+ totalElements: {
63
+ type: PropType<number | null>;
64
+ default: null;
65
+ };
66
+ /** Optional items array (passed to the row template as `item`). */
67
+ items: {
68
+ type: PropType<readonly unknown[] | null>;
69
+ default: null;
70
+ };
71
+ /** Optional lazy getter for very large/sparse datasets (alternative to `items`). */
72
+ getItem: {
73
+ type: PropType<(index: number) => unknown>;
74
+ default: undefined;
75
+ };
76
+ /** Render prop alternative to the `#item` scoped slot. */
77
+ renderItem: {
78
+ type: PropType<(item: unknown, index: number) => VNodeChild>;
79
+ default: undefined;
80
+ };
81
+ /** Options forwarded to `new CeriousScroll(...)` (read once, at creation). */
82
+ options: {
83
+ type: PropType<CeriousScrollOptions>;
84
+ default: undefined;
85
+ };
86
+ /** Automatically render after scroll/resize/data changes. Default: `true`. */
87
+ autoRender: {
88
+ type: BooleanConstructor;
89
+ default: boolean;
90
+ };
91
+ }>> & Readonly<{
92
+ "onViewport-change"?: ((_detail: CeriousViewportChangeDetail) => any) | undefined;
93
+ "onMeasured-viewport"?: ((_range: MeasuredViewportRange) => any) | undefined;
94
+ onReady?: ((_scroller: unknown) => any) | undefined;
95
+ }>, {
96
+ totalElements: number | null;
97
+ items: readonly unknown[] | null;
98
+ getItem: (index: number) => unknown;
99
+ renderItem: (item: unknown, index: number) => VNodeChild;
100
+ options: CeriousScrollOptions;
101
+ autoRender: boolean;
102
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * DOM helpers shared by the Vue bindings.
3
+ *
4
+ * Copyright (c) 2024-2026 Cerious DevTech LLC. All rights reserved.
5
+ */
6
+ /** Attribute marking the dedicated content element rows are rendered into. */
7
+ export declare const CONTENT_ATTR = "data-cerious-scroll-content";
8
+ /** Attribute marking the inner mount node Vue renders each row into. */
9
+ export declare const ROW_ATTR = "data-cerious-scroll-row";
10
+ /**
11
+ * Ensure a dedicated, recyclable content element exists inside `container`.
12
+ *
13
+ * CeriousScroll renders/recycles row containers (clearing them with
14
+ * `textContent`/`innerHTML`) inside this element, while the native scrollbar
15
+ * and event listeners stay attached to the outer `container`. Keeping them
16
+ * separate prevents the scrollbar DOM from being wiped during rendering.
17
+ */
18
+ export declare function ensureContentElement(container: HTMLElement): HTMLElement;
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("vue"),A=require("@ceriousdevtech/cerious-scroll"),V="data-cerious-scroll-content",N="data-cerious-scroll-row";function _(e){const u=e.querySelector(`[${V}]`);if(u)return u;const o=document.createElement("div");return o.setAttribute(V,""),o.style.position="relative",o.style.width="100%",o.style.height="100%",o.style.overflow="hidden",e.appendChild(o),o}function q(e,u){const o=l=>{const r=l.detail;r&&u({percentage:r.percentage,currentElement:r.currentElement,scrollOffset:r.scrollOffset,result:{element:r.result.element,offset:r.result.offset}})},f=l=>{const r=l.detail;r&&u({percentage:r.percentage,currentElement:r.element,scrollOffset:r.scrollOffset,result:{element:r.element,offset:r.scrollOffset}})};return e.addEventListener("cerious-viewport-change",o),e.addEventListener("viewport-change",f),()=>{e.removeEventListener("cerious-viewport-change",o),e.removeEventListener("viewport-change",f)}}function I(e,u){const o=typeof e=="number"?e:typeof u=="number"?u:void 0;if(o===void 0||Number.isNaN(o))throw new Error("useCeriousScroll: provide `totalElements` or `items`.");return Math.max(1,Math.floor(o))}function j(e){var O;const u=(O=n.getCurrentInstance())==null?void 0:O.appContext,o=n.ref(null),f=n.shallowRef(null);let l=null;const r=new Map;let s=null;const C=()=>n.toValue(e.autoRender)??!0,y=t=>{if(e.getItem)return e.getItem(t);const c=n.toValue(e.items);return c?c[t]:void 0},M=(t,c)=>{const i=u??null,m=n.effectScope(!0);return m.run(()=>n.watchEffect(()=>{const E=e.renderItem(y(t),t),d=Array.isArray(E)?E:[E];for(const w of d)n.isVNode(w)&&(w.appContext=i);const a=n.h(n.Fragment,d);a.appContext=i,n.render(a,c)},{flush:"sync"})),()=>m.stop()},b=t=>{t.stop(),n.render(null,t.mount),t.mount.remove()},v=()=>{var w;if(!l)return null;const{scroller:t,contentEl:c,container:i}=l,m=i.clientHeight||i.offsetHeight||0,E=(g,h)=>{const p=document.createElement("div");p.setAttribute(N,String(g)),h.appendChild(p);const S=M(g,p);r.set(g,{el:h,mount:p,stop:S})},d=t.renderViewport(m,c,E),a=new Set(t.getRenderedIndices());return r.forEach((g,h)=>{a.has(h)||(b(g),r.delete(h))}),(w=e.onMeasuredViewport)==null||w.call(e,d),d},R=t=>{if(!l)return;const c=l;l=null,c.unsubscribe(),t&&(s={currentElement:c.scroller.currentElement,scrollOffset:c.scroller.scrollOffset}),r.forEach(i=>b(i)),r.clear(),c.contentEl.textContent="",c.scroller.detachScrollbar(c.container),c.scroller.dispose(),f.value=null},T=t=>{var g,h;const c=_(t),i=e.options??{},m=i.onScroll,E={...i,onScroll:()=>{m==null||m(),C()&&v()}},d=I(n.toValue(e.totalElements),((g=n.toValue(e.items))==null?void 0:g.length)??null),a=new A.CeriousScroll(t,d,E);s&&(a.currentElement=Math.min(s.currentElement,d-1),a.scrollOffset=s.scrollOffset,s=null);const w=q(t,p=>{var S;(S=e.onViewportChange)==null||S.call(e,p)});l={scroller:a,contentEl:c,container:t,unsubscribe:w},f.value=a,(h=e.onReady)==null||h.call(e,a),C()&&requestAnimationFrame(()=>v())},P=()=>{R(!0);const t=o.value;t&&T(t)};return n.onMounted(()=>{const t=o.value;t&&T(t)}),n.onBeforeUnmount(()=>{R(!1)}),n.watch(()=>[n.toValue(e.totalElements),n.toValue(e.items)],()=>{var c;if(!l)return;const t=I(n.toValue(e.totalElements),((c=n.toValue(e.items))==null?void 0:c.length)??null);l.scroller.totalElements!==t&&P()}),{containerRef:o,scroller:f,render:v,jumpToElement:t=>l?(l.scroller.jumpToElement(t),v()):null,scrollToPercentage:t=>l?(l.scroller.handleScrollPercentage(t),v()):null,reset:()=>l?(l.scroller.reset(),v()):null,recalculate:()=>l?(l.scroller.clearAllCaches(),v()):null}}const F=n.defineComponent({name:"CeriousScroll",props:{totalElements:{type:Number,default:null},items:{type:Array,default:null},getItem:{type:Function,default:void 0},renderItem:{type:Function,default:void 0},options:{type:Object,default:void 0},autoRender:{type:Boolean,default:!0}},emits:{"viewport-change":e=>!0,"measured-viewport":e=>!0,ready:e=>!0},setup(e,{slots:u,emit:o,expose:f}){const l=(s,C)=>{var y;return e.renderItem?e.renderItem(s,C):(y=u.item)==null?void 0:y.call(u,{item:s,index:C})},r=j({totalElements:()=>e.totalElements,items:()=>e.items,getItem:e.getItem?s=>e.getItem(s):void 0,renderItem:l,options:e.options,autoRender:()=>e.autoRender,onViewportChange:s=>o("viewport-change",s),onMeasuredViewport:s=>o("measured-viewport",s),onReady:s=>o("ready",s)});return f({scroller:r.scroller,render:r.render,jumpToElement:r.jumpToElement,scrollToPercentage:r.scrollToPercentage,reset:r.reset,recalculate:r.recalculate}),()=>n.h("div",{ref:r.containerRef,style:{position:"relative",overflow:"hidden"}})}});Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>A.CeriousScroll});exports.CeriousScroll=F;exports.useCeriousScroll=j;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @ceriousdevtech/vue-cerious-scroll
3
+ *
4
+ * Vue 3 bindings for the CeriousScroll virtual scrolling engine.
5
+ *
6
+ * Copyright (c) 2024-2026 Cerious DevTech LLC. All rights reserved.
7
+ */
8
+ export { CeriousScroll } from './cerious-scroll';
9
+ export { useCeriousScroll } from './use-cerious-scroll';
10
+ export type { UseCeriousScrollOptions, UseCeriousScrollResult, } from './use-cerious-scroll';
11
+ export type { CeriousViewportChangeDetail } from './viewport-change';
12
+ export { CeriousScroll as CeriousScrollEngine } from '@ceriousdevtech/cerious-scroll';
13
+ export type { CeriousScrollOptions, KeyboardNavigationOptions, TouchNavigationOptions, WheelNavigationOptions, ElementRenderer, ElementHeightCalculator, ScrollResult, MeasuredViewportRange, } from '@ceriousdevtech/cerious-scroll';
package/dist/index.js ADDED
@@ -0,0 +1,189 @@
1
+ import { getCurrentInstance as P, ref as _, shallowRef as x, onMounted as F, onBeforeUnmount as L, watch as $, toValue as h, render as O, effectScope as q, watchEffect as B, isVNode as H, h as M, Fragment as U, defineComponent as W } from "vue";
2
+ import { CeriousScroll as z } from "@ceriousdevtech/cerious-scroll";
3
+ import { CeriousScroll as oe } from "@ceriousdevtech/cerious-scroll";
4
+ const A = "data-cerious-scroll-content", D = "data-cerious-scroll-row";
5
+ function G(e) {
6
+ const s = e.querySelector(`[${A}]`);
7
+ if (s) return s;
8
+ const n = document.createElement("div");
9
+ return n.setAttribute(A, ""), n.style.position = "relative", n.style.width = "100%", n.style.height = "100%", n.style.overflow = "hidden", e.appendChild(n), n;
10
+ }
11
+ function J(e, s) {
12
+ const n = (o) => {
13
+ const r = o.detail;
14
+ r && s({
15
+ percentage: r.percentage,
16
+ currentElement: r.currentElement,
17
+ scrollOffset: r.scrollOffset,
18
+ result: { element: r.result.element, offset: r.result.offset }
19
+ });
20
+ }, a = (o) => {
21
+ const r = o.detail;
22
+ r && s({
23
+ percentage: r.percentage,
24
+ currentElement: r.element,
25
+ scrollOffset: r.scrollOffset,
26
+ result: { element: r.element, offset: r.scrollOffset }
27
+ });
28
+ };
29
+ return e.addEventListener("cerious-viewport-change", n), e.addEventListener("viewport-change", a), () => {
30
+ e.removeEventListener("cerious-viewport-change", n), e.removeEventListener("viewport-change", a);
31
+ };
32
+ }
33
+ function V(e, s) {
34
+ const n = typeof e == "number" ? e : typeof s == "number" ? s : void 0;
35
+ if (n === void 0 || Number.isNaN(n))
36
+ throw new Error("useCeriousScroll: provide `totalElements` or `items`.");
37
+ return Math.max(1, Math.floor(n));
38
+ }
39
+ function K(e) {
40
+ var I;
41
+ const s = (I = P()) == null ? void 0 : I.appContext, n = _(null), a = x(null);
42
+ let o = null;
43
+ const r = /* @__PURE__ */ new Map();
44
+ let c = null;
45
+ const C = () => h(e.autoRender) ?? !0, y = (t) => {
46
+ if (e.getItem) return e.getItem(t);
47
+ const l = h(e.items);
48
+ return l ? l[t] : void 0;
49
+ }, N = (t, l) => {
50
+ const u = s ?? null, f = q(!0);
51
+ return f.run(
52
+ () => B(
53
+ () => {
54
+ const E = e.renderItem(y(t), t), m = Array.isArray(E) ? E : [E];
55
+ for (const w of m)
56
+ H(w) && (w.appContext = u);
57
+ const i = M(U, m);
58
+ i.appContext = u, O(i, l);
59
+ },
60
+ { flush: "sync" }
61
+ )
62
+ ), () => f.stop();
63
+ }, S = (t) => {
64
+ t.stop(), O(null, t.mount), t.mount.remove();
65
+ }, v = () => {
66
+ var w;
67
+ if (!o) return null;
68
+ const { scroller: t, contentEl: l, container: u } = o, f = u.clientHeight || u.offsetHeight || 0, E = (d, g) => {
69
+ const p = document.createElement("div");
70
+ p.setAttribute(D, String(d)), g.appendChild(p);
71
+ const R = N(d, p);
72
+ r.set(d, { el: g, mount: p, stop: R });
73
+ }, m = t.renderViewport(f, l, E), i = new Set(t.getRenderedIndices());
74
+ return r.forEach((d, g) => {
75
+ i.has(g) || (S(d), r.delete(g));
76
+ }), (w = e.onMeasuredViewport) == null || w.call(e, m), m;
77
+ }, T = (t) => {
78
+ if (!o) return;
79
+ const l = o;
80
+ o = null, l.unsubscribe(), t && (c = {
81
+ currentElement: l.scroller.currentElement,
82
+ scrollOffset: l.scroller.scrollOffset
83
+ }), r.forEach((u) => S(u)), r.clear(), l.contentEl.textContent = "", l.scroller.detachScrollbar(l.container), l.scroller.dispose(), a.value = null;
84
+ }, b = (t) => {
85
+ var d, g;
86
+ const l = G(t), u = e.options ?? {}, f = u.onScroll, E = {
87
+ ...u,
88
+ onScroll: () => {
89
+ f == null || f(), C() && v();
90
+ }
91
+ }, m = V(h(e.totalElements), ((d = h(e.items)) == null ? void 0 : d.length) ?? null), i = new z(t, m, E);
92
+ c && (i.currentElement = Math.min(c.currentElement, m - 1), i.scrollOffset = c.scrollOffset, c = null);
93
+ const w = J(t, (p) => {
94
+ var R;
95
+ (R = e.onViewportChange) == null || R.call(e, p);
96
+ });
97
+ o = { scroller: i, contentEl: l, container: t, unsubscribe: w }, a.value = i, (g = e.onReady) == null || g.call(e, i), C() && requestAnimationFrame(() => v());
98
+ }, j = () => {
99
+ T(!0);
100
+ const t = n.value;
101
+ t && b(t);
102
+ };
103
+ return F(() => {
104
+ const t = n.value;
105
+ t && b(t);
106
+ }), L(() => {
107
+ T(!1);
108
+ }), $(
109
+ () => [h(e.totalElements), h(e.items)],
110
+ () => {
111
+ var l;
112
+ if (!o) return;
113
+ const t = V(h(e.totalElements), ((l = h(e.items)) == null ? void 0 : l.length) ?? null);
114
+ o.scroller.totalElements !== t && j();
115
+ }
116
+ ), {
117
+ containerRef: n,
118
+ scroller: a,
119
+ render: v,
120
+ jumpToElement: (t) => o ? (o.scroller.jumpToElement(t), v()) : null,
121
+ scrollToPercentage: (t) => o ? (o.scroller.handleScrollPercentage(t), v()) : null,
122
+ reset: () => o ? (o.scroller.reset(), v()) : null,
123
+ recalculate: () => o ? (o.scroller.clearAllCaches(), v()) : null
124
+ };
125
+ }
126
+ const te = W({
127
+ name: "CeriousScroll",
128
+ props: {
129
+ /** Total item count. Falls back to `items.length` when omitted. */
130
+ totalElements: { type: Number, default: null },
131
+ /** Optional items array (passed to the row template as `item`). */
132
+ items: { type: Array, default: null },
133
+ /** Optional lazy getter for very large/sparse datasets (alternative to `items`). */
134
+ getItem: { type: Function, default: void 0 },
135
+ /** Render prop alternative to the `#item` scoped slot. */
136
+ renderItem: {
137
+ type: Function,
138
+ default: void 0
139
+ },
140
+ /** Options forwarded to `new CeriousScroll(...)` (read once, at creation). */
141
+ options: { type: Object, default: void 0 },
142
+ /** Automatically render after scroll/resize/data changes. Default: `true`. */
143
+ autoRender: { type: Boolean, default: !0 }
144
+ },
145
+ emits: {
146
+ /** Normalized viewport-change payload (wheel/touch/keyboard/scrollbar). */
147
+ "viewport-change": (e) => !0,
148
+ /** Measured range after each render pass. */
149
+ "measured-viewport": (e) => !0,
150
+ /** Emitted once the underlying engine instance is ready (and after recreation). */
151
+ ready: (e) => !0
152
+ },
153
+ setup(e, { slots: s, emit: n, expose: a }) {
154
+ const o = (c, C) => {
155
+ var y;
156
+ return e.renderItem ? e.renderItem(c, C) : (y = s.item) == null ? void 0 : y.call(s, { item: c, index: C });
157
+ }, r = K({
158
+ totalElements: () => e.totalElements,
159
+ items: () => e.items,
160
+ getItem: e.getItem ? (c) => e.getItem(c) : void 0,
161
+ renderItem: o,
162
+ // Engine options are consumed at creation; reading the prop once matches
163
+ // that contract (remount via `:key` to apply new engine options).
164
+ options: e.options,
165
+ autoRender: () => e.autoRender,
166
+ onViewportChange: (c) => n("viewport-change", c),
167
+ onMeasuredViewport: (c) => n("measured-viewport", c),
168
+ onReady: (c) => n("ready", c)
169
+ });
170
+ return a({
171
+ /** The underlying engine instance (`null` before mount). */
172
+ scroller: r.scroller,
173
+ render: r.render,
174
+ jumpToElement: r.jumpToElement,
175
+ scrollToPercentage: r.scrollToPercentage,
176
+ reset: r.reset,
177
+ recalculate: r.recalculate
178
+ }), () => M("div", {
179
+ ref: r.containerRef,
180
+ // User-supplied class/style/attrs fall through and merge onto this root.
181
+ style: { position: "relative", overflow: "hidden" }
182
+ });
183
+ }
184
+ });
185
+ export {
186
+ te as CeriousScroll,
187
+ oe as CeriousScrollEngine,
188
+ K as useCeriousScroll
189
+ };
@@ -0,0 +1,61 @@
1
+ import { MaybeRefOrGetter, Ref, VNodeChild } from 'vue';
2
+ import { CeriousScroll as CeriousScrollEngine, CeriousScrollOptions, MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
3
+ import { CeriousViewportChangeDetail } from './viewport-change';
4
+ export interface UseCeriousScrollOptions<TItem = unknown> {
5
+ /** Total number of items. Falls back to `items.length` when omitted. */
6
+ totalElements?: MaybeRefOrGetter<number | null | undefined>;
7
+ /** Optional items array (passed to `renderItem` as the first argument). */
8
+ items?: MaybeRefOrGetter<readonly TItem[] | null | undefined>;
9
+ /** Optional getter for very large/sparse datasets (alternative to `items`). */
10
+ getItem?: (index: number) => TItem;
11
+ /** Renders a single row to a Vue VNode. `item` is `undefined` with no data source. */
12
+ renderItem: (item: TItem, index: number) => VNodeChild;
13
+ /** Options forwarded to `new CeriousScroll(...)` (read once, at creation). */
14
+ options?: CeriousScrollOptions;
15
+ /** Automatically render after scroll/resize/data changes. Default: `true`. */
16
+ autoRender?: MaybeRefOrGetter<boolean | undefined>;
17
+ /** Invoked with the normalized viewport-change payload. */
18
+ onViewportChange?: (detail: CeriousViewportChangeDetail) => void;
19
+ /** Invoked with the measured range after each render pass. */
20
+ onMeasuredViewport?: (range: MeasuredViewportRange) => void;
21
+ /** Invoked once the underlying engine instance is ready (and after recreation). */
22
+ onReady?: (scroller: CeriousScrollEngine) => void;
23
+ }
24
+ export interface UseCeriousScrollResult {
25
+ /** Attach to the scroll container element (`<div :ref="containerRef">`). */
26
+ containerRef: Ref<HTMLElement | null>;
27
+ /** The underlying engine instance (`null` before mount / after unmount). */
28
+ scroller: Ref<CeriousScrollEngine | null>;
29
+ /** Imperatively trigger a render pass. */
30
+ render: () => MeasuredViewportRange | null;
31
+ /** Jump to an element index, then render. */
32
+ jumpToElement: (index: number) => MeasuredViewportRange | null;
33
+ /** Scroll to a percentage (0..100), then render. */
34
+ scrollToPercentage: (percentage: number) => MeasuredViewportRange | null;
35
+ /** Reset to the top, then render. */
36
+ reset: () => MeasuredViewportRange | null;
37
+ /**
38
+ * Discard all cached row heights and re-measure the viewport.
39
+ *
40
+ * Call this only when the heights of rows you've *already rendered* may have
41
+ * changed without their indices changing — e.g. a global font/density change,
42
+ * or swapping every row to a different layout. This forces a synchronous
43
+ * re-measure (one `offsetHeight` read per visible row), so do NOT call it on
44
+ * routine edits: a single cell edit doesn't need it (its row keeps its size,
45
+ * and the engine's ResizeObserver picks up any incidental resize on its own).
46
+ */
47
+ recalculate: () => MeasuredViewportRange | null;
48
+ }
49
+ /**
50
+ * Bind a CeriousScroll engine to a container and render rows with Vue.
51
+ *
52
+ * Each row is rendered into an inner mount node (which lives inside the engine's
53
+ * recyclable container) via Vue's synchronous `render(vnode, el)`, so the engine
54
+ * measures the row's real height — no estimation. Because rendering is
55
+ * synchronous, no `flushSync`-style escape hatch is needed.
56
+ *
57
+ * Rows are rendered with the host component's `appContext`, so globally
58
+ * registered components, directives, and installed plugins are available inside
59
+ * each row.
60
+ */
61
+ export declare function useCeriousScroll<TItem = unknown>(opts: UseCeriousScrollOptions<TItem>): UseCeriousScrollResult;
@@ -0,0 +1,25 @@
1
+ import { ScrollResult } from '@ceriousdevtech/cerious-scroll';
2
+ /**
3
+ * Normalized payload for viewport changes emitted by CeriousScroll.
4
+ *
5
+ * Mirrors the `cerious-viewport-change` CustomEvent dispatched on wheel/touch/
6
+ * keyboard navigation, and normalizes the native scrollbar's `viewport-change`
7
+ * event into the same shape.
8
+ */
9
+ export interface CeriousViewportChangeDetail {
10
+ /** Scroll percentage from 0..100. */
11
+ percentage: number;
12
+ /** Current top-most element index tracked by CeriousScroll. */
13
+ currentElement: number;
14
+ /** Pixel offset within `currentElement`. */
15
+ scrollOffset: number;
16
+ /** Scroll operation result (element + offset). */
17
+ result: ScrollResult;
18
+ }
19
+ /**
20
+ * Subscribe to both viewport-change events emitted by CeriousScroll and invoke
21
+ * `callback` with a normalized {@link CeriousViewportChangeDetail}.
22
+ *
23
+ * @returns An unsubscribe function.
24
+ */
25
+ export declare function subscribeViewportChange(container: HTMLElement, callback: (detail: CeriousViewportChangeDetail) => void): () => void;
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@ceriousdevtech/vue-cerious-scroll",
3
+ "version": "1.0.1",
4
+ "description": "Vue 3 bindings for CeriousScroll — high-performance virtual scrolling with O(1) memory and no height estimation",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "sideEffects": false,
24
+ "license": "MIT",
25
+ "author": "Cerious DevTech LLC <info@ceriousdevtech.com>",
26
+ "homepage": "https://github.com/ceriousdevtech/vue-cerious-scroll#readme",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/ceriousdevtech/vue-cerious-scroll.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/ceriousdevtech/vue-cerious-scroll/issues",
33
+ "email": "info@ceriousdevtech.com"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "keywords": [
39
+ "vue",
40
+ "vue3",
41
+ "virtual-scroll",
42
+ "virtual-scrolling",
43
+ "infinite-scroll",
44
+ "performance",
45
+ "large-lists",
46
+ "data-grid",
47
+ "variable-height",
48
+ "o1-memory",
49
+ "cerious-scroll"
50
+ ],
51
+ "scripts": {
52
+ "build": "vite build",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "typecheck": "vue-tsc --noEmit",
56
+ "demo": "vite --config demo/vite.config.ts",
57
+ "demo:build": "vite build --config demo/vite.config.ts"
58
+ },
59
+ "peerDependencies": {
60
+ "@ceriousdevtech/cerious-scroll": "^1.0.1",
61
+ "vue": "^3.3.0"
62
+ },
63
+ "devDependencies": {
64
+ "@ceriousdevtech/cerious-scroll": "^1.0.1",
65
+ "@types/node": "^22.10.2",
66
+ "@vitejs/plugin-vue": "^5.2.1",
67
+ "@vue/test-utils": "^2.4.6",
68
+ "jsdom": "^25.0.1",
69
+ "typescript": "^5.6.3",
70
+ "vite": "^5.4.11",
71
+ "vite-plugin-dts": "^4.3.0",
72
+ "vitest": "^2.1.8",
73
+ "vue": "^3.5.13",
74
+ "vue-router": "^4.6.4",
75
+ "vue-tsc": "^2.1.10"
76
+ }
77
+ }