@ceriousdevtech/vue-cerious-scroll 1.0.4 → 1.0.5

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 CHANGED
@@ -5,6 +5,15 @@ All notable changes to vue-cerious-scroll will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.5] - 2026-06-08
9
+
10
+ ### Added
11
+ - **Table mode support** (`:options="{ layout: 'table' }"`). Rows render as real `<td>` cells into the engine's `<tr>`. Each row renders into its own `display: contents` wrapper, isolating Vue's renderer from the engine's `<tr>` recycling (so a fast scroll can't tear nodes out from under Vue) while the cells still lay out as the row's columns.
12
+ - **`#header` slot**. Declarative header (a `<tr>` of `<th>`s) rendered into the engine's `<thead>` in its own reactive effect — same `<table>` as the rows, so columns align natively, and interactive headers (sort state, etc.) update on their own.
13
+
14
+ ### Dependencies
15
+ - Bumped `@ceriousdevtech/cerious-scroll` to `^1.0.6`: native table layout, `table.autoSizeColumns` (auto-sized but stable columns), trackpad-only wheel inertia, overlay-scrollbar gutter fix, and exact bottom snap.
16
+
8
17
  ## [1.0.4] - 2026-06-04
9
18
 
10
19
  ### Dependencies
package/README.md CHANGED
@@ -113,8 +113,9 @@ const { containerRef } = useCeriousScroll({
113
113
  | `autoRender` | `boolean` | Re-render on scroll/resize/data changes. Default `true`. |
114
114
 
115
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!).
116
+ `render-item` prop. In table mode, a **`#header` slot** renders the `<thead>` row
117
+ (see [Table layout](#table-layout)). Apply `class` / `style` directly to the
118
+ component — they fall through onto the scroll container (set a height!).
118
119
 
119
120
  ### Events
120
121
 
@@ -138,6 +139,36 @@ const scroll = ref<InstanceType<typeof CeriousScroll> | null>(null);
138
139
 
139
140
  ---
140
141
 
142
+ ## Table layout
143
+
144
+ Pass `:options="{ layout: 'table' }"` to render real `<table>` / `<tr>` / `<td>` rows with a frozen header and native column alignment. The `#item` slot returns the row's `<td>` cells; a `#header` slot provides the (declarative, reactive) `<thead>` row:
145
+
146
+ ```vue
147
+ <CeriousScroll
148
+ class="my-scroll"
149
+ :total-elements="100000"
150
+ :get-item="(i) => i"
151
+ :options="{ layout: 'table', table: { tableClassName: 'my-table', autoSizeColumns: true } }"
152
+ >
153
+ <template #header>
154
+ <tr><th v-for="c in columns" :key="c.key">{{ c.label }}</th></tr>
155
+ </template>
156
+
157
+ <template #item="{ item: index }">
158
+ <td>{{ row(index).id }}</td>
159
+ <td>{{ row(index).name }}</td>
160
+ <td>{{ row(index).email }}</td>
161
+ </template>
162
+ </CeriousScroll>
163
+ ```
164
+
165
+ - The **`#header` slot** renders into the engine's `<thead>` (same `<table>` as the rows → native column alignment, frozen header) and stays reactive.
166
+ - The **`#item` slot must return `<td>`s**. They render into the row's `<tr>` via a `display: contents` wrapper that isolates Vue's renderer from the engine's row recycling.
167
+ - **`table.autoSizeColumns`** measures column widths once and pins them (auto-sized + stable); or use `table.columnWidths`. Variable row heights work as usual.
168
+ - CSS: `border-collapse: separate` and an **opaque `<thead>` background** (see the core README's [Table Layout](https://github.com/ceriousdevtech/cerious-scroll#-table-layout-layout-table) notes).
169
+
170
+ ---
171
+
141
172
  ## Notes
142
173
 
143
174
  - **No height estimation.** Rows are committed with Vue's synchronous `render()`
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("vue"),A=require("@ceriousdevtech/cerious-scroll"),V="data-cerious-scroll-content",N="data-cerious-scroll-row";function _(e){const s=e.querySelector(`[${V}]`);if(s)return s;const n=document.createElement("div");return n.setAttribute(V,""),n.style.position="relative",n.style.width="100%",n.style.height="100%",n.style.overflowY="clip",n.style.overflowX="auto",e.appendChild(n),n}function q(e,s){const n=l=>{const r=l.detail;r&&s({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&&s({percentage:r.percentage,currentElement:r.element,scrollOffset:r.scrollOffset,result:{element:r.element,offset:r.scrollOffset}})};return e.addEventListener("cerious-viewport-change",n),e.addEventListener("viewport-change",f),()=>{e.removeEventListener("cerious-viewport-change",n),e.removeEventListener("viewport-change",f)}}function I(e,s){const n=typeof e=="number"?e:typeof s=="number"?s:void 0;if(n===void 0||Number.isNaN(n))throw new Error("useCeriousScroll: provide `totalElements` or `items`.");return Math.max(1,Math.floor(n))}function j(e){var O;const s=(O=o.getCurrentInstance())==null?void 0:O.appContext,n=o.ref(null),f=o.shallowRef(null);let l=null;const r=new Map;let c=null;const C=()=>o.toValue(e.autoRender)??!0,y=t=>{if(e.getItem)return e.getItem(t);const u=o.toValue(e.items);return u?u[t]:void 0},M=(t,u)=>{const i=s??null,m=o.effectScope(!0);return m.run(()=>o.watchEffect(()=>{const E=e.renderItem(y(t),t),d=Array.isArray(E)?E:[E];for(const w of d)o.isVNode(w)&&(w.appContext=i);const a=o.h(o.Fragment,d);a.appContext=i,o.render(a,u)},{flush:"sync"})),()=>m.stop()},b=t=>{t.stop(),o.render(null,t.mount),t.mount.remove()},h=()=>{var w;if(!l)return null;const{scroller:t,contentEl:u,container:i}=l,m=i.clientHeight||i.offsetHeight||0,E=(g,v)=>{const p=document.createElement("div");p.setAttribute(N,String(g)),v.appendChild(p);const S=M(g,p);r.set(g,{el:v,mount:p,stop:S})},d=t.renderViewport(m,u,E),a=new Set(t.getRenderedIndices());return r.forEach((g,v)=>{a.has(v)||(b(g),r.delete(v))}),(w=e.onMeasuredViewport)==null||w.call(e,d),d},R=t=>{if(!l)return;const u=l;l=null,u.unsubscribe(),t&&(c={currentElement:u.scroller.currentElement,scrollOffset:u.scroller.scrollOffset}),r.forEach(i=>b(i)),r.clear(),u.contentEl.textContent="",u.scroller.detachScrollbar(u.container),u.scroller.dispose(),f.value=null},T=t=>{var g,v;const u=_(t),i=e.options??{},m=i.onScroll,E={...i,onScroll:()=>{m==null||m(),C()&&h()}},d=I(o.toValue(e.totalElements),((g=o.toValue(e.items))==null?void 0:g.length)??null),a=new A.CeriousScroll(t,d,E);c&&(a.currentElement=Math.min(c.currentElement,d-1),a.scrollOffset=c.scrollOffset,c=null);const w=q(t,p=>{var S;(S=e.onViewportChange)==null||S.call(e,p)});l={scroller:a,contentEl:u,container:t,unsubscribe:w},f.value=a,(v=e.onReady)==null||v.call(e,a),C()&&requestAnimationFrame(()=>h())},P=()=>{R(!0);const t=n.value;t&&T(t)};return o.onMounted(()=>{const t=n.value;t&&T(t)}),o.onBeforeUnmount(()=>{R(!1)}),o.watch(()=>[o.toValue(e.totalElements),o.toValue(e.items)],()=>{var u;if(!l)return;const t=I(o.toValue(e.totalElements),((u=o.toValue(e.items))==null?void 0:u.length)??null);l.scroller.totalElements!==t&&P()}),{containerRef:n,scroller:f,render:h,jumpToElement:t=>l?(l.scroller.jumpToElement(t),h()):null,scrollToPercentage:t=>l?(l.scroller.handleScrollPercentage(t),h()):null,reset:()=>l?(l.scroller.reset(),h()):null,recalculate:()=>l?(l.scroller.clearAllCaches(),h()):null}}const F=o.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:s,emit:n,expose:f}){const l=(c,C)=>{var y;return e.renderItem?e.renderItem(c,C):(y=s.item)==null?void 0:y.call(s,{item:c,index:C})},r=j({totalElements:()=>e.totalElements,items:()=>e.items,getItem:e.getItem?c=>e.getItem(c):void 0,renderItem:l,options:e.options,autoRender:()=>e.autoRender,onViewportChange:c=>n("viewport-change",c),onMeasuredViewport:c=>n("measured-viewport",c),onReady:c=>n("ready",c)});return f({scroller:r.scroller,render:r.render,jumpToElement:r.jumpToElement,scrollToPercentage:r.scrollToPercentage,reset:r.reset,recalculate:r.recalculate}),()=>{var c;return o.h("div",{ref:r.containerRef,style:{position:"relative",overflow:"hidden"}},(c=s.default)==null?void 0:c.call(s))}}});Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>A.CeriousScroll});exports.CeriousScroll=F;exports.useCeriousScroll=j;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("vue"),N=require("@ceriousdevtech/cerious-scroll"),M="data-cerious-scroll-content",q="data-cerious-scroll-row";function L(e){const u=e.querySelector(`[${M}]`);if(u)return u;const o=document.createElement("div");return o.setAttribute(M,""),o.style.position="relative",o.style.width="100%",o.style.height="100%",o.style.overflowY="clip",o.style.overflowX="auto",e.appendChild(o),o}function B(e,u){const o=l=>{const n=l.detail;n&&u({percentage:n.percentage,currentElement:n.currentElement,scrollOffset:n.scrollOffset,result:{element:n.result.element,offset:n.result.offset}})},g=l=>{const n=l.detail;n&&u({percentage:n.percentage,currentElement:n.element,scrollOffset:n.scrollOffset,result:{element:n.element,offset:n.scrollOffset}})};return e.addEventListener("cerious-viewport-change",o),e.addEventListener("viewport-change",g),()=>{e.removeEventListener("cerious-viewport-change",o),e.removeEventListener("viewport-change",g)}}function j(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 P(e){var I,A;const u=(I=r.getCurrentInstance())==null?void 0:I.appContext,o=r.ref(null),g=r.shallowRef(null);let l=null;const n=new Map;let s=null;const b=((A=e.options)==null?void 0:A.layout)==="table";let d=null;const H=t=>{if(!e.renderHeader)return;d==null||d();const c=u??null,a=r.effectScope(!0);a.run(()=>r.watchEffect(()=>{const f=e.renderHeader(),m=Array.isArray(f)?f:[f];for(const i of m)r.isVNode(i)&&(i.appContext=c);const h=r.h(r.Fragment,m);h.appContext=c,r.render(h,t)},{flush:"sync"})),d=()=>a.stop()},R=()=>r.toValue(e.autoRender)??!0,x=t=>{if(e.getItem)return e.getItem(t);const c=r.toValue(e.items);return c?c[t]:void 0},F=(t,c)=>{const a=u??null,f=r.effectScope(!0);return f.run(()=>r.watchEffect(()=>{const m=e.renderItem(x(t),t),h=Array.isArray(m)?m:[m];for(const p of h)r.isVNode(p)&&(p.appContext=a);const i=r.h(r.Fragment,h);i.appContext=a,r.render(i,c)},{flush:"sync"})),()=>f.stop()},T=t=>{t.stop(),r.render(null,t.mount),t.mount.remove()},y=()=>{var p;if(!l)return null;const{scroller:t,contentEl:c,container:a}=l,f=a.clientHeight||a.offsetHeight||0,m=(v,E)=>{const w=document.createElement("div");b&&(w.style.display="contents"),w.setAttribute(q,String(v)),E.appendChild(w);const C=F(v,w);n.set(v,{el:E,mount:w,stop:C})},h=t.renderViewport(f,c,m),i=new Set(t.getRenderedIndices());return n.forEach((v,E)=>{i.has(E)||(T(v),n.delete(E))}),(p=e.onMeasuredViewport)==null||p.call(e,h),h},V=t=>{if(!l)return;const c=l;l=null,c.unsubscribe(),t&&(s={currentElement:c.scroller.currentElement,scrollOffset:c.scroller.scrollOffset}),n.forEach(a=>T(a)),n.clear(),d==null||d(),d=null,c.contentEl.textContent="",c.scroller.detachScrollbar(c.container),c.scroller.dispose(),g.value=null},O=t=>{var v,E,w;const c=L(t),a=e.options??{},f=a.onScroll,m={...a,onScroll:()=>{f==null||f(),R()&&y()}};if(b&&e.renderHeader){const C=(v=a.table)==null?void 0:v.header;m.table={...a.table,header:S=>{C==null||C(S),H(S)}}}const h=j(r.toValue(e.totalElements),((E=r.toValue(e.items))==null?void 0:E.length)??null),i=new N.CeriousScroll(t,h,m);s&&(i.currentElement=Math.min(s.currentElement,h-1),i.scrollOffset=s.scrollOffset,s=null);const p=B(t,C=>{var S;(S=e.onViewportChange)==null||S.call(e,C)});l={scroller:i,contentEl:c,container:t,unsubscribe:p},g.value=i,(w=e.onReady)==null||w.call(e,i),R()&&requestAnimationFrame(()=>y())},_=()=>{V(!0);const t=o.value;t&&O(t)};return r.onMounted(()=>{const t=o.value;t&&O(t)}),r.onBeforeUnmount(()=>{V(!1)}),r.watch(()=>[r.toValue(e.totalElements),r.toValue(e.items)],()=>{var c;if(!l)return;const t=j(r.toValue(e.totalElements),((c=r.toValue(e.items))==null?void 0:c.length)??null);l.scroller.totalElements!==t&&_()}),{containerRef:o,scroller:g,render:y,jumpToElement:t=>l?(l.scroller.jumpToElement(t),y()):null,scrollToPercentage:t=>l?(l.scroller.handleScrollPercentage(t),y()):null,reset:()=>l?(l.scroller.reset(),y()):null,recalculate:()=>l?(l.scroller.clearAllCaches(),y()):null}}const U=r.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:g}){const l=(s,b)=>{var d;return e.renderItem?e.renderItem(s,b):(d=u.item)==null?void 0:d.call(u,{item:s,index:b})},n=P({totalElements:()=>e.totalElements,items:()=>e.items,getItem:e.getItem?s=>e.getItem(s):void 0,renderItem:l,renderHeader:u.header?()=>u.header():void 0,options:e.options,autoRender:()=>e.autoRender,onViewportChange:s=>o("viewport-change",s),onMeasuredViewport:s=>o("measured-viewport",s),onReady:s=>o("ready",s)});return g({scroller:n.scroller,render:n.render,jumpToElement:n.jumpToElement,scrollToPercentage:n.scrollToPercentage,reset:n.reset,recalculate:n.recalculate}),()=>{var s;return r.h("div",{ref:n.containerRef,style:{position:"relative",overflow:"hidden"}},(s=u.default)==null?void 0:s.call(u))}}});Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>N.CeriousScroll});exports.CeriousScroll=U;exports.useCeriousScroll=P;
package/dist/index.js CHANGED
@@ -1,129 +1,156 @@
1
- import { getCurrentInstance as P, ref as _, shallowRef as x, onMounted as F, onBeforeUnmount as L, watch as $, toValue as v, 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 X } from "@ceriousdevtech/cerious-scroll";
3
- import { CeriousScroll as oe } from "@ceriousdevtech/cerious-scroll";
4
- const A = "data-cerious-scroll-content", Y = "data-cerious-scroll-row";
5
- function z(e) {
6
- const s = e.querySelector(`[${A}]`);
7
- if (s) return s;
1
+ import { getCurrentInstance as U, ref as W, shallowRef as X, onMounted as Y, onBeforeUnmount as z, watch as D, toValue as w, effectScope as N, watchEffect as j, isVNode as H, h as I, Fragment as P, render as T, defineComponent as G } from "vue";
2
+ import { CeriousScroll as J } from "@ceriousdevtech/cerious-scroll";
3
+ import { CeriousScroll as ie } from "@ceriousdevtech/cerious-scroll";
4
+ const _ = "data-cerious-scroll-content", K = "data-cerious-scroll-row";
5
+ function Q(e) {
6
+ const c = e.querySelector(`[${_}]`);
7
+ if (c) return c;
8
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.overflowY = "clip", n.style.overflowX = "auto", e.appendChild(n), n;
9
+ return n.setAttribute(_, ""), n.style.position = "relative", n.style.width = "100%", n.style.height = "100%", n.style.overflowY = "clip", n.style.overflowX = "auto", e.appendChild(n), n;
10
10
  }
11
- function D(e, s) {
11
+ function Z(e, c) {
12
12
  const n = (o) => {
13
13
  const r = o.detail;
14
- r && s({
14
+ r && c({
15
15
  percentage: r.percentage,
16
16
  currentElement: r.currentElement,
17
17
  scrollOffset: r.scrollOffset,
18
18
  result: { element: r.result.element, offset: r.result.offset }
19
19
  });
20
- }, a = (o) => {
20
+ }, h = (o) => {
21
21
  const r = o.detail;
22
- r && s({
22
+ r && c({
23
23
  percentage: r.percentage,
24
24
  currentElement: r.element,
25
25
  scrollOffset: r.scrollOffset,
26
26
  result: { element: r.element, offset: r.scrollOffset }
27
27
  });
28
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);
29
+ return e.addEventListener("cerious-viewport-change", n), e.addEventListener("viewport-change", h), () => {
30
+ e.removeEventListener("cerious-viewport-change", n), e.removeEventListener("viewport-change", h);
31
31
  };
32
32
  }
33
- function V(e, s) {
34
- const n = typeof e == "number" ? e : typeof s == "number" ? s : void 0;
33
+ function F(e, c) {
34
+ const n = typeof e == "number" ? e : typeof c == "number" ? c : void 0;
35
35
  if (n === void 0 || Number.isNaN(n))
36
36
  throw new Error("useCeriousScroll: provide `totalElements` or `items`.");
37
37
  return Math.max(1, Math.floor(n));
38
38
  }
39
- function G(e) {
40
- var I;
41
- const s = (I = P()) == null ? void 0 : I.appContext, n = _(null), a = x(null);
39
+ function k(e) {
40
+ var V, x;
41
+ const c = (V = U()) == null ? void 0 : V.appContext, n = W(null), h = X(null);
42
42
  let o = null;
43
43
  const r = /* @__PURE__ */ new Map();
44
- let l = null;
45
- const C = () => v(e.autoRender) ?? !0, y = (t) => {
44
+ let s = null;
45
+ const b = ((x = e.options) == null ? void 0 : x.layout) === "table";
46
+ let f = null;
47
+ const L = (t) => {
48
+ if (!e.renderHeader) return;
49
+ f == null || f();
50
+ const l = c ?? null, u = N(!0);
51
+ u.run(
52
+ () => j(
53
+ () => {
54
+ const a = e.renderHeader(), d = Array.isArray(a) ? a : [a];
55
+ for (const i of d) H(i) && (i.appContext = l);
56
+ const m = I(P, d);
57
+ m.appContext = l, T(m, t);
58
+ },
59
+ { flush: "sync" }
60
+ )
61
+ ), f = () => u.stop();
62
+ }, O = () => w(e.autoRender) ?? !0, $ = (t) => {
46
63
  if (e.getItem) return e.getItem(t);
47
- const c = v(e.items);
48
- return c ? c[t] : void 0;
49
- }, N = (t, c) => {
50
- const u = s ?? null, f = q(!0);
51
- return f.run(
52
- () => B(
64
+ const l = w(e.items);
65
+ return l ? l[t] : void 0;
66
+ }, q = (t, l) => {
67
+ const u = c ?? null, a = N(!0);
68
+ return a.run(
69
+ () => j(
53
70
  () => {
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, c);
71
+ const d = e.renderItem($(t), t), m = Array.isArray(d) ? d : [d];
72
+ for (const y of m)
73
+ H(y) && (y.appContext = u);
74
+ const i = I(P, m);
75
+ i.appContext = u, T(i, l);
59
76
  },
60
77
  { flush: "sync" }
61
78
  )
62
- ), () => f.stop();
79
+ ), () => a.stop();
63
80
  }, S = (t) => {
64
- t.stop(), O(null, t.mount), t.mount.remove();
65
- }, h = () => {
66
- var w;
81
+ t.stop(), T(null, t.mount), t.mount.remove();
82
+ }, p = () => {
83
+ var y;
67
84
  if (!o) return null;
68
- const { scroller: t, contentEl: c, container: u } = o, f = u.clientHeight || u.offsetHeight || 0, E = (d, g) => {
69
- const p = document.createElement("div");
70
- p.setAttribute(Y, 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, c, 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) => {
85
+ const { scroller: t, contentEl: l, container: u } = o, a = u.clientHeight || u.offsetHeight || 0, d = (v, g) => {
86
+ const E = document.createElement("div");
87
+ b && (E.style.display = "contents"), E.setAttribute(K, String(v)), g.appendChild(E);
88
+ const C = q(v, E);
89
+ r.set(v, { el: g, mount: E, stop: C });
90
+ }, m = t.renderViewport(a, l, d), i = new Set(t.getRenderedIndices());
91
+ return r.forEach((v, g) => {
92
+ i.has(g) || (S(v), r.delete(g));
93
+ }), (y = e.onMeasuredViewport) == null || y.call(e, m), m;
94
+ }, A = (t) => {
78
95
  if (!o) return;
79
- const c = o;
80
- o = null, c.unsubscribe(), t && (l = {
81
- currentElement: c.scroller.currentElement,
82
- scrollOffset: c.scroller.scrollOffset
83
- }), r.forEach((u) => S(u)), r.clear(), c.contentEl.textContent = "", c.scroller.detachScrollbar(c.container), c.scroller.dispose(), a.value = null;
84
- }, b = (t) => {
85
- var d, g;
86
- const c = z(t), u = e.options ?? {}, f = u.onScroll, E = {
96
+ const l = o;
97
+ o = null, l.unsubscribe(), t && (s = {
98
+ currentElement: l.scroller.currentElement,
99
+ scrollOffset: l.scroller.scrollOffset
100
+ }), r.forEach((u) => S(u)), r.clear(), f == null || f(), f = null, l.contentEl.textContent = "", l.scroller.detachScrollbar(l.container), l.scroller.dispose(), h.value = null;
101
+ }, M = (t) => {
102
+ var v, g, E;
103
+ const l = Q(t), u = e.options ?? {}, a = u.onScroll, d = {
87
104
  ...u,
88
105
  onScroll: () => {
89
- f == null || f(), C() && h();
106
+ a == null || a(), O() && p();
90
107
  }
91
- }, m = V(v(e.totalElements), ((d = v(e.items)) == null ? void 0 : d.length) ?? null), i = new X(t, m, E);
92
- l && (i.currentElement = Math.min(l.currentElement, m - 1), i.scrollOffset = l.scrollOffset, l = null);
93
- const w = D(t, (p) => {
108
+ };
109
+ if (b && e.renderHeader) {
110
+ const C = (v = u.table) == null ? void 0 : v.header;
111
+ d.table = {
112
+ ...u.table,
113
+ header: (R) => {
114
+ C == null || C(R), L(R);
115
+ }
116
+ };
117
+ }
118
+ const m = F(w(e.totalElements), ((g = w(e.items)) == null ? void 0 : g.length) ?? null), i = new J(t, m, d);
119
+ s && (i.currentElement = Math.min(s.currentElement, m - 1), i.scrollOffset = s.scrollOffset, s = null);
120
+ const y = Z(t, (C) => {
94
121
  var R;
95
- (R = e.onViewportChange) == null || R.call(e, p);
122
+ (R = e.onViewportChange) == null || R.call(e, C);
96
123
  });
97
- o = { scroller: i, contentEl: c, container: t, unsubscribe: w }, a.value = i, (g = e.onReady) == null || g.call(e, i), C() && requestAnimationFrame(() => h());
98
- }, j = () => {
99
- T(!0);
124
+ o = { scroller: i, contentEl: l, container: t, unsubscribe: y }, h.value = i, (E = e.onReady) == null || E.call(e, i), O() && requestAnimationFrame(() => p());
125
+ }, B = () => {
126
+ A(!0);
100
127
  const t = n.value;
101
- t && b(t);
128
+ t && M(t);
102
129
  };
103
- return F(() => {
130
+ return Y(() => {
104
131
  const t = n.value;
105
- t && b(t);
106
- }), L(() => {
107
- T(!1);
108
- }), $(
109
- () => [v(e.totalElements), v(e.items)],
132
+ t && M(t);
133
+ }), z(() => {
134
+ A(!1);
135
+ }), D(
136
+ () => [w(e.totalElements), w(e.items)],
110
137
  () => {
111
- var c;
138
+ var l;
112
139
  if (!o) return;
113
- const t = V(v(e.totalElements), ((c = v(e.items)) == null ? void 0 : c.length) ?? null);
114
- o.scroller.totalElements !== t && j();
140
+ const t = F(w(e.totalElements), ((l = w(e.items)) == null ? void 0 : l.length) ?? null);
141
+ o.scroller.totalElements !== t && B();
115
142
  }
116
143
  ), {
117
144
  containerRef: n,
118
- scroller: a,
119
- render: h,
120
- jumpToElement: (t) => o ? (o.scroller.jumpToElement(t), h()) : null,
121
- scrollToPercentage: (t) => o ? (o.scroller.handleScrollPercentage(t), h()) : null,
122
- reset: () => o ? (o.scroller.reset(), h()) : null,
123
- recalculate: () => o ? (o.scroller.clearAllCaches(), h()) : null
145
+ scroller: h,
146
+ render: p,
147
+ jumpToElement: (t) => o ? (o.scroller.jumpToElement(t), p()) : null,
148
+ scrollToPercentage: (t) => o ? (o.scroller.handleScrollPercentage(t), p()) : null,
149
+ reset: () => o ? (o.scroller.reset(), p()) : null,
150
+ recalculate: () => o ? (o.scroller.clearAllCaches(), p()) : null
124
151
  };
125
152
  }
126
- const te = W({
153
+ const ce = G({
127
154
  name: "CeriousScroll",
128
155
  props: {
129
156
  /** Total item count. Falls back to `items.length` when omitted. */
@@ -150,24 +177,27 @@ const te = W({
150
177
  /** Emitted once the underlying engine instance is ready (and after recreation). */
151
178
  ready: (e) => !0
152
179
  },
153
- setup(e, { slots: s, emit: n, expose: a }) {
154
- const o = (l, C) => {
155
- var y;
156
- return e.renderItem ? e.renderItem(l, C) : (y = s.item) == null ? void 0 : y.call(s, { item: l, index: C });
157
- }, r = G({
180
+ setup(e, { slots: c, emit: n, expose: h }) {
181
+ const o = (s, b) => {
182
+ var f;
183
+ return e.renderItem ? e.renderItem(s, b) : (f = c.item) == null ? void 0 : f.call(c, { item: s, index: b });
184
+ }, r = k({
158
185
  totalElements: () => e.totalElements,
159
186
  items: () => e.items,
160
- getItem: e.getItem ? (l) => e.getItem(l) : void 0,
187
+ getItem: e.getItem ? (s) => e.getItem(s) : void 0,
161
188
  renderItem: o,
189
+ // Table mode: declarative header. Render the `#header` slot (a <tr> of
190
+ // <th>s) into the engine's <thead>. `undefined` when no slot is provided.
191
+ renderHeader: c.header ? () => c.header() : void 0,
162
192
  // Engine options are consumed at creation; reading the prop once matches
163
193
  // that contract (remount via `:key` to apply new engine options).
164
194
  options: e.options,
165
195
  autoRender: () => e.autoRender,
166
- onViewportChange: (l) => n("viewport-change", l),
167
- onMeasuredViewport: (l) => n("measured-viewport", l),
168
- onReady: (l) => n("ready", l)
196
+ onViewportChange: (s) => n("viewport-change", s),
197
+ onMeasuredViewport: (s) => n("measured-viewport", s),
198
+ onReady: (s) => n("ready", s)
169
199
  });
170
- return a({
200
+ return h({
171
201
  /** The underlying engine instance (`null` before mount). */
172
202
  scroller: r.scroller,
173
203
  render: r.render,
@@ -176,21 +206,21 @@ const te = W({
176
206
  reset: r.reset,
177
207
  recalculate: r.recalculate
178
208
  }), () => {
179
- var l;
180
- return M(
209
+ var s;
210
+ return I(
181
211
  "div",
182
212
  {
183
213
  ref: r.containerRef,
184
214
  // User-supplied class/style/attrs fall through and merge onto this root.
185
215
  style: { position: "relative", overflow: "hidden" }
186
216
  },
187
- (l = s.default) == null ? void 0 : l.call(s)
217
+ (s = c.default) == null ? void 0 : s.call(c)
188
218
  );
189
219
  };
190
220
  }
191
221
  });
192
222
  export {
193
- te as CeriousScroll,
194
- oe as CeriousScrollEngine,
195
- G as useCeriousScroll
223
+ ce as CeriousScroll,
224
+ ie as CeriousScrollEngine,
225
+ k as useCeriousScroll
196
226
  };
@@ -10,6 +10,11 @@ export interface UseCeriousScrollOptions<TItem = unknown> {
10
10
  getItem?: (index: number) => TItem;
11
11
  /** Renders a single row to a Vue VNode. `item` is `undefined` with no data source. */
12
12
  renderItem: (item: TItem, index: number) => VNodeChild;
13
+ /**
14
+ * Table mode only: renders the header (a `<tr>` of `<th>`s) into the engine's
15
+ * `<thead>`. Reactive — re-renders when its dependencies change.
16
+ */
17
+ renderHeader?: () => VNodeChild;
13
18
  /** Options forwarded to `new CeriousScroll(...)` (read once, at creation). */
14
19
  options?: CeriousScrollOptions;
15
20
  /** Automatically render after scroll/resize/data changes. Default: `true`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceriousdevtech/vue-cerious-scroll",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Vue 3 bindings for CeriousScroll — high-performance virtual scrolling with O(1) memory and no height estimation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -57,7 +57,7 @@
57
57
  "demo:build": "vite build --config demo/vite.config.ts"
58
58
  },
59
59
  "dependencies": {
60
- "@ceriousdevtech/cerious-scroll": "^1.0.5"
60
+ "@ceriousdevtech/cerious-scroll": "^1.0.6"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "vue": "^3.3.0"