@ceriousdevtech/react-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 react-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>`. React renders each row into its own `display: contents` wrapper, which keeps the reconciler isolated from the engine's `<tr>` recycling (so a fast scroll can't trip a `removeChild` on cells the engine already cleared) while the cells still lay out as the row's columns.
12
+ - **`tableHeader`** prop. Declarative header (provide a `<tr>` of `<th>`s) rendered into the engine's `<thead>` via a portal — same `<table>` as the rows, so columns align natively. Reactive like any other portal.
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
@@ -109,7 +109,8 @@ function List() {
109
109
  | `items` | `readonly TItem[]` | Optional data array. `totalElements` defaults to `items.length`. |
110
110
  | `totalElements` | `number` | Total item count. Required if `items` is omitted. |
111
111
  | `getItem` | `(index) => TItem` | Lazy item getter for large/sparse datasets. |
112
- | `options` | `CeriousScrollOptions` | Engine options (keyboard/touch/wheel/scrollbar/etc.). Read once at creation. |
112
+ | `tableHeader` | `ReactNode` | Table mode only. A `<tr>` of `<th>`s rendered into the engine's `<thead>` (see [Table layout](#table-layout)). |
113
+ | `options` | `CeriousScrollOptions` | Engine options (keyboard/touch/wheel/scrollbar/`layout`/etc.). Read once at creation. |
113
114
  | `autoRender` | `boolean` | Re-render on scroll/resize/data changes. Default `true`. |
114
115
  | `onViewportChange` | `(detail) => void` | Normalized viewport-change callback. |
115
116
  | `onMeasuredViewport` | `(range) => void` | Measured range after each render pass. |
@@ -130,6 +131,41 @@ const ref = useRef<CeriousScrollHandle>(null);
130
131
 
131
132
  ---
132
133
 
134
+ ## Table layout
135
+
136
+ Pass `options={{ layout: 'table' }}` to render real `<table>` / `<tr>` / `<td>` rows with a frozen header and native column alignment. Your `renderItem` returns the row's `<td>` cells, and `tableHeader` provides the (declarative, reactive) `<thead>` row:
137
+
138
+ ```tsx
139
+ import { TABLE_COLUMNS } from './data';
140
+
141
+ <CeriousScroll
142
+ className="my-scroll" // give it a height
143
+ totalElements={100_000}
144
+ getItem={(i) => i}
145
+ options={{ layout: 'table', table: { tableClassName: 'my-table', autoSizeColumns: true } }}
146
+ tableHeader={
147
+ <tr>{TABLE_COLUMNS.map((c) => <th key={c.key}>{c.label}</th>)}</tr>
148
+ }
149
+ renderItem={(index) => {
150
+ const row = makeRow(index);
151
+ return (
152
+ <>
153
+ <td>{row.id}</td>
154
+ <td>{row.name}</td>
155
+ <td>{row.email}</td>
156
+ </>
157
+ );
158
+ }}
159
+ />
160
+ ```
161
+
162
+ - **`tableHeader`** is portaled into the engine's `<thead>` — the same `<table>` as the rows, so columns align natively and the header stays frozen.
163
+ - **`renderItem` must return `<td>`s** (a fragment of cells). They're rendered into the row's `<tr>` via a `display: contents` wrapper, so React fully owns them and the engine's row recycling can't tear them out.
164
+ - **`table.autoSizeColumns`** measures column widths once and pins them — auto-sized but stable (no jitter, no manual widths). Or use `table.columnWidths`. Variable row heights work as usual.
165
+ - 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).
166
+
167
+ ---
168
+
133
169
  ## Notes
134
170
 
135
171
  - **No height estimation.** Rows are committed with `flushSync` so the engine
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const Q=require("react/jsx-runtime"),r=require("react"),D=require("react-dom"),U=require("react-dom/server"),W=require("@ceriousdevtech/cerious-scroll"),B="data-cerious-scroll-content",Z="data-cerious-scroll-row";function x(e){const l=e.querySelector(`[${B}]`);if(l)return l;const n=document.createElement("div");return n.setAttribute(B,""),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 ee(e,l){const n=d=>{const o=d.detail;o&&l({percentage:o.percentage,currentElement:o.currentElement,scrollOffset:o.scrollOffset,result:{element:o.result.element,offset:o.result.offset}})},i=d=>{const o=d.detail;o&&l({percentage:o.percentage,currentElement:o.element,scrollOffset:o.scrollOffset,result:{element:o.element,offset:o.scrollOffset}})};return e.addEventListener("cerious-viewport-change",n),e.addEventListener("viewport-change",i),()=>{e.removeEventListener("cerious-viewport-change",n),e.removeEventListener("viewport-change",i)}}function te(e,l){const n=typeof e=="number"?e:typeof l=="number"?l: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 X(e){var z;const l=r.useRef(null),n=r.useRef(null),i=r.useRef(new Map),d=r.useRef(new Map),o=r.useRef(null),g=r.useRef(null),[,j]=r.useReducer(t=>t+1,0),[k,b]=r.useState(null),p=r.useRef(e.renderItem),R=r.useRef(e.items??null),S=r.useRef(e.getItem),w=r.useRef(e.options),E=r.useRef(e.autoRender??!0),L=r.useRef(e.totalElements??null),O=r.useRef(e.onViewportChange),T=r.useRef(e.onMeasuredViewport),M=r.useRef(e.onReady);p.current=e.renderItem,R.current=e.items??null,S.current=e.getItem,w.current=e.options,E.current=e.autoRender??!0,L.current=e.totalElements??null,O.current=e.onViewportChange,T.current=e.onMeasuredViewport,M.current=e.onReady;const N=r.useCallback(t=>{const c=S.current;if(c)return c(t);const a=R.current;return a?a[t]:void 0},[]),s=r.useCallback(()=>{var I;const t=n.current;if(!t)return null;const{scroller:c,contentEl:a,container:u}=t,F=u.clientHeight||u.offsetHeight||0,y=[],f=(m,C)=>{const h=document.createElement("div");h.setAttribute(Z,String(m)),h.innerHTML=U.renderToStaticMarkup(r.createElement(r.Fragment,null,p.current(N(m),m))),C.appendChild(h),i.current.set(m,{el:C,mount:h}),y.push(h)},A=c.renderViewport(F,a,f),q=new Set(c.getRenderedIndices());return i.current.forEach((m,C)=>{q.has(C)||i.current.delete(C)}),y.forEach(m=>{m.textContent=""}),D.flushSync(j),(I=T.current)==null||I.call(T,A),A},[]),Y=e.totalElements??((z=e.items)==null?void 0:z.length)??null;r.useEffect(()=>{var I,m;const t=l.current;if(!t)return;const c=x(t),a=w.current??{},u=a.onScroll,F={...a,onScroll:()=>{u==null||u(),E.current&&s()}},y=te(L.current,((I=R.current)==null?void 0:I.length)??null),f=new W.CeriousScroll(t,y,F);g.current&&(f.currentElement=Math.min(g.current.currentElement,y-1),f.scrollOffset=g.current.scrollOffset,g.current=null),n.current={scroller:f,contentEl:c,container:t},b(f);const A=ee(t,C=>{var h;(h=O.current)==null||h.call(O,C)});(m=M.current)==null||m.call(M,f);let q=0;return E.current&&(q=requestAnimationFrame(()=>s())),()=>{cancelAnimationFrame(q),A(),g.current={currentElement:f.currentElement,scrollOffset:f.scrollOffset},i.current.clear(),d.current.clear(),o.current=null,c.textContent="",f.detachScrollbar(t),f.dispose(),n.current=null,b(null)}},[Y,s]),r.useEffect(()=>{if(!n.current||!E.current)return;const t=requestAnimationFrame(()=>s());return()=>cancelAnimationFrame(t)},[e.items,e.getItem]);const $=r.useCallback(t=>{const c=n.current;return c?(c.scroller.jumpToElement(t),s()):null},[s]),G=r.useCallback(t=>{const c=n.current;return c?(c.scroller.handleScrollPercentage(t),s()):null},[s]),J=r.useCallback(()=>{const t=n.current;return t?(t.scroller.reset(),s()):null},[s]),K=r.useCallback(()=>{const t=n.current;return t?(t.scroller.clearAllCaches(),s()):null},[s]),H=R.current,V=!S.current&&H!=null?H.length:null,P=p.current,v=d.current;o.current!==P&&(v.clear(),o.current=P);const _=[];return i.current.forEach((t,c)=>{if(V!==null&&c>=V){v.delete(c);return}const a=N(c);let u=v.get(c);(!u||u.item!==a||u.mount!==t.mount)&&(u={item:a,mount:t.mount,node:D.createPortal(P(a,c),t.mount,String(c))},v.set(c,u)),_.push(u.node)}),v.size>i.current.size&&v.forEach((t,c)=>{i.current.has(c)||v.delete(c)}),{containerRef:l,portals:_,scroller:k,render:s,jumpToElement:$,scrollToPercentage:G,reset:J,recalculate:K}}function re(e,l){const{className:n,style:i,"data-testid":d,children:o,...g}=e,{containerRef:j,portals:k,scroller:b,render:p,jumpToElement:R,scrollToPercentage:S,reset:w,recalculate:E}=X(g);return r.useImperativeHandle(l,()=>({scroller:b,render:p,jumpToElement:R,scrollToPercentage:S,reset:w,recalculate:E}),[b,p,R,S,w,E]),Q.jsxs("div",{ref:j,className:n,"data-testid":d,style:{position:"relative",overflow:"hidden",...i},children:[o,k]})}const ne=r.forwardRef(re);Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>W.CeriousScroll});exports.CeriousScroll=ne;exports.useCeriousScroll=X;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const ee=require("react/jsx-runtime"),r=require("react"),z=require("react-dom"),te=require("react-dom/server"),G=require("@ceriousdevtech/cerious-scroll"),$="data-cerious-scroll-content",re="data-cerious-scroll-row";function ne(e){const s=e.querySelector(`[${$}]`);if(s)return s;const n=document.createElement("div");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}function ce(e,s){const n=h=>{const l=h.detail;l&&s({percentage:l.percentage,currentElement:l.currentElement,scrollOffset:l.scrollOffset,result:{element:l.result.element,offset:l.result.offset}})},f=h=>{const l=h.detail;l&&s({percentage:l.percentage,currentElement:l.element,scrollOffset:l.scrollOffset,result:{element:l.element,offset:l.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 le(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 Y;const s=r.useRef(null),n=r.useRef(null),f=r.useRef(new Map),h=r.useRef(new Map),l=r.useRef(null),R=r.useRef(null),S=r.useRef(null),T=r.useRef(e.tableHeader),[,M]=r.useReducer(t=>t+1,0),[A,p]=r.useState(null),b=r.useRef(e.renderItem),g=r.useRef(e.items??null),v=r.useRef(e.getItem),L=r.useRef(e.options),q=r.useRef(e.autoRender??!0),D=r.useRef(e.totalElements??null),H=r.useRef(e.onViewportChange),P=r.useRef(e.onMeasuredViewport),j=r.useRef(e.onReady);b.current=e.renderItem,g.current=e.items??null,v.current=e.getItem,T.current=e.tableHeader,L.current=e.options,q.current=e.autoRender??!0,D.current=e.totalElements??null,H.current=e.onViewportChange,P.current=e.onMeasuredViewport,j.current=e.onReady;const B=r.useCallback(t=>{const c=v.current;if(c)return c(t);const o=g.current;return o?o[t]:void 0},[]),u=r.useCallback(()=>{var I,O;const t=n.current;if(!t)return null;const{scroller:c,contentEl:o,container:a}=t,k=a.clientHeight||a.offsetHeight||0,w=[],m=((I=L.current)==null?void 0:I.layout)==="table",_=(i,d)=>{const C=document.createElement("div");m&&(C.style.display="contents"),C.setAttribute(re,String(i)),C.innerHTML=te.renderToStaticMarkup(r.createElement(r.Fragment,null,b.current(B(i),i))),d.appendChild(C),f.current.set(i,{el:d,mount:C}),w.push(C)},y=c.renderViewport(k,o,_),F=new Set(c.getRenderedIndices());return f.current.forEach((i,d)=>{F.has(d)||f.current.delete(d)}),w.forEach(i=>{i.textContent=""}),z.flushSync(M),(O=P.current)==null||O.call(P,y),y},[]),K=e.totalElements??((Y=e.items)==null?void 0:Y.length)??null;r.useEffect(()=>{var F,I,O;const t=s.current;if(!t)return;const c=ne(t),o=L.current??{},a=o.onScroll,k={...o,onScroll:()=>{a==null||a(),q.current&&u()}};if(o.layout==="table"&&T.current!=null){const i=(F=o.table)==null?void 0:F.header;k.table={...o.table,header:d=>{S.current=d,i==null||i(d)}}}const w=le(D.current,((I=g.current)==null?void 0:I.length)??null),m=new G.CeriousScroll(t,w,k);R.current&&(m.currentElement=Math.min(R.current.currentElement,w-1),m.scrollOffset=R.current.scrollOffset,R.current=null),n.current={scroller:m,contentEl:c,container:t},p(m);const _=ce(t,i=>{var d;(d=H.current)==null||d.call(H,i)});(O=j.current)==null||O.call(j,m);let y=0;return q.current&&(y=requestAnimationFrame(()=>u())),()=>{cancelAnimationFrame(y),_(),R.current={currentElement:m.currentElement,scrollOffset:m.scrollOffset},f.current.clear(),h.current.clear(),l.current=null,c.textContent="",m.detachScrollbar(t),m.dispose(),n.current=null,p(null)}},[K,u]),r.useEffect(()=>{if(!n.current||!q.current)return;const t=requestAnimationFrame(()=>u());return()=>cancelAnimationFrame(t)},[e.items,e.getItem]);const Q=r.useCallback(t=>{const c=n.current;return c?(c.scroller.jumpToElement(t),u()):null},[u]),U=r.useCallback(t=>{const c=n.current;return c?(c.scroller.handleScrollPercentage(t),u()):null},[u]),Z=r.useCallback(()=>{const t=n.current;return t?(t.scroller.reset(),u()):null},[u]),x=r.useCallback(()=>{const t=n.current;return t?(t.scroller.clearAllCaches(),u()):null},[u]),W=g.current,X=!v.current&&W!=null?W.length:null,N=b.current,E=h.current;l.current!==N&&(E.clear(),l.current=N);const V=[];return f.current.forEach((t,c)=>{if(X!==null&&c>=X){E.delete(c);return}const o=B(c);let a=E.get(c);(!a||a.item!==o||a.mount!==t.mount)&&(a={item:o,mount:t.mount,node:z.createPortal(N(o,c),t.mount,String(c))},E.set(c,a)),V.push(a.node)}),E.size>f.current.size&&E.forEach((t,c)=>{f.current.has(c)||E.delete(c)}),e.tableHeader!=null&&S.current&&V.push(z.createPortal(e.tableHeader,S.current,"cerious-thead")),{containerRef:s,portals:V,scroller:A,render:u,jumpToElement:Q,scrollToPercentage:U,reset:Z,recalculate:x}}function oe(e,s){const{className:n,style:f,"data-testid":h,children:l,...R}=e,{containerRef:S,portals:T,scroller:M,render:A,jumpToElement:p,scrollToPercentage:b,reset:g,recalculate:v}=J(R);return r.useImperativeHandle(s,()=>({scroller:M,render:A,jumpToElement:p,scrollToPercentage:b,reset:g,recalculate:v}),[M,A,p,b,g,v]),ee.jsxs("div",{ref:S,className:n,"data-testid":h,style:{position:"relative",overflow:"hidden",...f},children:[l,T]})}const se=r.forwardRef(oe);Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>G.CeriousScroll});exports.CeriousScroll=se;exports.useCeriousScroll=J;
package/dist/index.js CHANGED
@@ -1,175 +1,185 @@
1
- import { jsxs as K } from "react/jsx-runtime";
2
- import { useRef as c, useReducer as Q, useState as U, useCallback as S, useEffect as B, createElement as Z, Fragment as x, forwardRef as ee, useImperativeHandle as te } from "react";
3
- import { flushSync as re, createPortal as ne } from "react-dom";
4
- import { renderToStaticMarkup as oe } from "react-dom/server";
5
- import { CeriousScroll as ce } from "@ceriousdevtech/cerious-scroll";
6
- import { CeriousScroll as we } from "@ceriousdevtech/cerious-scroll";
7
- const D = "data-cerious-scroll-content", le = "data-cerious-scroll-row";
8
- function se(e) {
9
- const l = e.querySelector(`[${D}]`);
10
- if (l) return l;
1
+ import { jsxs as ee } from "react/jsx-runtime";
2
+ import { useRef as o, useReducer as te, useState as re, useCallback as w, useEffect as Y, createElement as ne, Fragment as ce, forwardRef as oe, useImperativeHandle as le } from "react";
3
+ import { flushSync as se, createPortal as G } from "react-dom";
4
+ import { renderToStaticMarkup as ue } from "react-dom/server";
5
+ import { CeriousScroll as ae } from "@ceriousdevtech/cerious-scroll";
6
+ import { CeriousScroll as ye } from "@ceriousdevtech/cerious-scroll";
7
+ const J = "data-cerious-scroll-content", ie = "data-cerious-scroll-row";
8
+ function fe(e) {
9
+ const s = e.querySelector(`[${J}]`);
10
+ if (s) return s;
11
11
  const r = document.createElement("div");
12
- return r.setAttribute(D, ""), r.style.position = "relative", r.style.width = "100%", r.style.height = "100%", r.style.overflowY = "clip", r.style.overflowX = "auto", e.appendChild(r), r;
12
+ return r.setAttribute(J, ""), r.style.position = "relative", r.style.width = "100%", r.style.height = "100%", r.style.overflowY = "clip", r.style.overflowX = "auto", e.appendChild(r), r;
13
13
  }
14
- function ue(e, l) {
15
- const r = (d) => {
16
- const o = d.detail;
17
- o && l({
18
- percentage: o.percentage,
19
- currentElement: o.currentElement,
20
- scrollOffset: o.scrollOffset,
21
- result: { element: o.result.element, offset: o.result.offset }
14
+ function me(e, s) {
15
+ const r = (h) => {
16
+ const c = h.detail;
17
+ c && s({
18
+ percentage: c.percentage,
19
+ currentElement: c.currentElement,
20
+ scrollOffset: c.scrollOffset,
21
+ result: { element: c.result.element, offset: c.result.offset }
22
22
  });
23
- }, i = (d) => {
24
- const o = d.detail;
25
- o && l({
26
- percentage: o.percentage,
27
- currentElement: o.element,
28
- scrollOffset: o.scrollOffset,
29
- result: { element: o.element, offset: o.scrollOffset }
23
+ }, f = (h) => {
24
+ const c = h.detail;
25
+ c && s({
26
+ percentage: c.percentage,
27
+ currentElement: c.element,
28
+ scrollOffset: c.scrollOffset,
29
+ result: { element: c.element, offset: c.scrollOffset }
30
30
  });
31
31
  };
32
- return e.addEventListener("cerious-viewport-change", r), e.addEventListener("viewport-change", i), () => {
33
- e.removeEventListener("cerious-viewport-change", r), e.removeEventListener("viewport-change", i);
32
+ return e.addEventListener("cerious-viewport-change", r), e.addEventListener("viewport-change", f), () => {
33
+ e.removeEventListener("cerious-viewport-change", r), e.removeEventListener("viewport-change", f);
34
34
  };
35
35
  }
36
- function ie(e, l) {
37
- const r = typeof e == "number" ? e : typeof l == "number" ? l : void 0;
36
+ function de(e, s) {
37
+ const r = typeof e == "number" ? e : typeof s == "number" ? s : void 0;
38
38
  if (r === void 0 || Number.isNaN(r))
39
39
  throw new Error("useCeriousScroll: provide `totalElements` or `items`.");
40
40
  return Math.max(1, Math.floor(r));
41
41
  }
42
- function ae(e) {
43
- var $;
44
- const l = c(null), r = c(null), i = c(/* @__PURE__ */ new Map()), d = c(/* @__PURE__ */ new Map()), o = c(null), g = c(null), [, N] = Q((t) => t + 1, 0), [P, I] = U(null), w = c(e.renderItem), E = c(e.items ?? null), C = c(e.getItem), T = c(e.options), p = c(e.autoRender ?? !0), V = c(e.totalElements ?? null), y = c(e.onViewportChange), A = c(e.onMeasuredViewport), M = c(e.onReady);
45
- w.current = e.renderItem, E.current = e.items ?? null, C.current = e.getItem, T.current = e.options, p.current = e.autoRender ?? !0, V.current = e.totalElements ?? null, y.current = e.onViewportChange, A.current = e.onMeasuredViewport, M.current = e.onReady;
46
- const _ = S((t) => {
47
- const n = C.current;
42
+ function he(e) {
43
+ var X;
44
+ const s = o(null), r = o(null), f = o(/* @__PURE__ */ new Map()), h = o(/* @__PURE__ */ new Map()), c = o(null), g = o(null), C = o(null), M = o(e.tableHeader), [, A] = te((t) => t + 1, 0), [H, S] = re(null), v = o(e.renderItem), E = o(e.items ?? null), R = o(e.getItem), _ = o(e.options), F = o(e.autoRender ?? !0), $ = o(e.totalElements ?? null), L = o(e.onViewportChange), N = o(e.onMeasuredViewport), P = o(e.onReady);
45
+ v.current = e.renderItem, E.current = e.items ?? null, R.current = e.getItem, M.current = e.tableHeader, _.current = e.options, F.current = e.autoRender ?? !0, $.current = e.totalElements ?? null, L.current = e.onViewportChange, N.current = e.onMeasuredViewport, P.current = e.onReady;
46
+ const B = w((t) => {
47
+ const n = R.current;
48
48
  if (n) return n(t);
49
- const a = E.current;
50
- return a ? a[t] : void 0;
51
- }, []), s = S(() => {
52
- var b;
49
+ const l = E.current;
50
+ return l ? l[t] : void 0;
51
+ }, []), u = w(() => {
52
+ var T, O;
53
53
  const t = r.current;
54
54
  if (!t) return null;
55
- const { scroller: n, contentEl: a, container: u } = t, H = u.clientHeight || u.offsetHeight || 0, O = [], f = (m, R) => {
56
- const h = document.createElement("div");
57
- h.setAttribute(le, String(m)), h.innerHTML = oe(
58
- Z(x, null, w.current(_(m), m))
59
- ), R.appendChild(h), i.current.set(m, { el: R, mount: h }), O.push(h);
60
- }, F = n.renderViewport(H, a, f), L = new Set(n.getRenderedIndices());
61
- return i.current.forEach((m, R) => {
62
- L.has(R) || i.current.delete(R);
63
- }), O.forEach((m) => {
64
- m.textContent = "";
65
- }), re(N), (b = A.current) == null || b.call(A, F), F;
66
- }, []), W = e.totalElements ?? (($ = e.items) == null ? void 0 : $.length) ?? null;
67
- B(() => {
68
- var b, m;
69
- const t = l.current;
55
+ const { scroller: n, contentEl: l, container: a } = t, j = a.clientHeight || a.offsetHeight || 0, y = [], m = ((T = _.current) == null ? void 0 : T.layout) === "table", z = (i, d) => {
56
+ const b = document.createElement("div");
57
+ m && (b.style.display = "contents"), b.setAttribute(ie, String(i)), b.innerHTML = ue(
58
+ ne(ce, null, v.current(B(i), i))
59
+ ), d.appendChild(b), f.current.set(i, { el: d, mount: b }), y.push(b);
60
+ }, I = n.renderViewport(j, l, z), V = new Set(n.getRenderedIndices());
61
+ return f.current.forEach((i, d) => {
62
+ V.has(d) || f.current.delete(d);
63
+ }), y.forEach((i) => {
64
+ i.textContent = "";
65
+ }), se(A), (O = N.current) == null || O.call(N, I), I;
66
+ }, []), K = e.totalElements ?? ((X = e.items) == null ? void 0 : X.length) ?? null;
67
+ Y(() => {
68
+ var V, T, O;
69
+ const t = s.current;
70
70
  if (!t) return;
71
- const n = se(t), a = T.current ?? {}, u = a.onScroll, H = {
72
- ...a,
71
+ const n = fe(t), l = _.current ?? {}, a = l.onScroll, j = {
72
+ ...l,
73
73
  onScroll: () => {
74
- u == null || u(), p.current && s();
74
+ a == null || a(), F.current && u();
75
75
  }
76
- }, O = ie(V.current, ((b = E.current) == null ? void 0 : b.length) ?? null), f = new ce(t, O, H);
77
- g.current && (f.currentElement = Math.min(g.current.currentElement, O - 1), f.scrollOffset = g.current.scrollOffset, g.current = null), r.current = { scroller: f, contentEl: n, container: t }, I(f);
78
- const F = ue(t, (R) => {
79
- var h;
80
- (h = y.current) == null || h.call(y, R);
76
+ };
77
+ if (l.layout === "table" && M.current != null) {
78
+ const i = (V = l.table) == null ? void 0 : V.header;
79
+ j.table = {
80
+ ...l.table,
81
+ header: (d) => {
82
+ C.current = d, i == null || i(d);
83
+ }
84
+ };
85
+ }
86
+ const y = de($.current, ((T = E.current) == null ? void 0 : T.length) ?? null), m = new ae(t, y, j);
87
+ g.current && (m.currentElement = Math.min(g.current.currentElement, y - 1), m.scrollOffset = g.current.scrollOffset, g.current = null), r.current = { scroller: m, contentEl: n, container: t }, S(m);
88
+ const z = me(t, (i) => {
89
+ var d;
90
+ (d = L.current) == null || d.call(L, i);
81
91
  });
82
- (m = M.current) == null || m.call(M, f);
83
- let L = 0;
84
- return p.current && (L = requestAnimationFrame(() => s())), () => {
85
- cancelAnimationFrame(L), F(), g.current = {
86
- currentElement: f.currentElement,
87
- scrollOffset: f.scrollOffset
88
- }, i.current.clear(), d.current.clear(), o.current = null, n.textContent = "", f.detachScrollbar(t), f.dispose(), r.current = null, I(null);
92
+ (O = P.current) == null || O.call(P, m);
93
+ let I = 0;
94
+ return F.current && (I = requestAnimationFrame(() => u())), () => {
95
+ cancelAnimationFrame(I), z(), g.current = {
96
+ currentElement: m.currentElement,
97
+ scrollOffset: m.scrollOffset
98
+ }, f.current.clear(), h.current.clear(), c.current = null, n.textContent = "", m.detachScrollbar(t), m.dispose(), r.current = null, S(null);
89
99
  };
90
- }, [W, s]), B(() => {
91
- if (!r.current || !p.current) return;
92
- const t = requestAnimationFrame(() => s());
100
+ }, [K, u]), Y(() => {
101
+ if (!r.current || !F.current) return;
102
+ const t = requestAnimationFrame(() => u());
93
103
  return () => cancelAnimationFrame(t);
94
104
  }, [e.items, e.getItem]);
95
- const X = S(
105
+ const Q = w(
96
106
  (t) => {
97
107
  const n = r.current;
98
- return n ? (n.scroller.jumpToElement(t), s()) : null;
108
+ return n ? (n.scroller.jumpToElement(t), u()) : null;
99
109
  },
100
- [s]
101
- ), Y = S(
110
+ [u]
111
+ ), U = w(
102
112
  (t) => {
103
113
  const n = r.current;
104
- return n ? (n.scroller.handleScrollPercentage(t), s()) : null;
114
+ return n ? (n.scroller.handleScrollPercentage(t), u()) : null;
105
115
  },
106
- [s]
107
- ), G = S(() => {
116
+ [u]
117
+ ), Z = w(() => {
108
118
  const t = r.current;
109
- return t ? (t.scroller.reset(), s()) : null;
110
- }, [s]), J = S(() => {
119
+ return t ? (t.scroller.reset(), u()) : null;
120
+ }, [u]), x = w(() => {
111
121
  const t = r.current;
112
- return t ? (t.scroller.clearAllCaches(), s()) : null;
113
- }, [s]), q = E.current, k = !C.current && q != null ? q.length : null, j = w.current, v = d.current;
114
- o.current !== j && (v.clear(), o.current = j);
115
- const z = [];
116
- return i.current.forEach((t, n) => {
117
- if (k !== null && n >= k) {
118
- v.delete(n);
122
+ return t ? (t.scroller.clearAllCaches(), u()) : null;
123
+ }, [u]), D = E.current, W = !R.current && D != null ? D.length : null, q = v.current, p = h.current;
124
+ c.current !== q && (p.clear(), c.current = q);
125
+ const k = [];
126
+ return f.current.forEach((t, n) => {
127
+ if (W !== null && n >= W) {
128
+ p.delete(n);
119
129
  return;
120
130
  }
121
- const a = _(n);
122
- let u = v.get(n);
123
- (!u || u.item !== a || u.mount !== t.mount) && (u = {
124
- item: a,
131
+ const l = B(n);
132
+ let a = p.get(n);
133
+ (!a || a.item !== l || a.mount !== t.mount) && (a = {
134
+ item: l,
125
135
  mount: t.mount,
126
- node: ne(j(a, n), t.mount, String(n))
127
- }, v.set(n, u)), z.push(u.node);
128
- }), v.size > i.current.size && v.forEach((t, n) => {
129
- i.current.has(n) || v.delete(n);
130
- }), {
131
- containerRef: l,
132
- portals: z,
133
- scroller: P,
134
- render: s,
135
- jumpToElement: X,
136
- scrollToPercentage: Y,
137
- reset: G,
138
- recalculate: J
136
+ node: G(q(l, n), t.mount, String(n))
137
+ }, p.set(n, a)), k.push(a.node);
138
+ }), p.size > f.current.size && p.forEach((t, n) => {
139
+ f.current.has(n) || p.delete(n);
140
+ }), e.tableHeader != null && C.current && k.push(G(e.tableHeader, C.current, "cerious-thead")), {
141
+ containerRef: s,
142
+ portals: k,
143
+ scroller: H,
144
+ render: u,
145
+ jumpToElement: Q,
146
+ scrollToPercentage: U,
147
+ reset: Z,
148
+ recalculate: x
139
149
  };
140
150
  }
141
- function fe(e, l) {
142
- const { className: r, style: i, "data-testid": d, children: o, ...g } = e, {
143
- containerRef: N,
144
- portals: P,
145
- scroller: I,
146
- render: w,
147
- jumpToElement: E,
148
- scrollToPercentage: C,
149
- reset: T,
150
- recalculate: p
151
- } = ae(g);
152
- return te(
153
- l,
154
- () => ({ scroller: I, render: w, jumpToElement: E, scrollToPercentage: C, reset: T, recalculate: p }),
155
- [I, w, E, C, T, p]
156
- ), /* @__PURE__ */ K(
151
+ function ge(e, s) {
152
+ const { className: r, style: f, "data-testid": h, children: c, ...g } = e, {
153
+ containerRef: C,
154
+ portals: M,
155
+ scroller: A,
156
+ render: H,
157
+ jumpToElement: S,
158
+ scrollToPercentage: v,
159
+ reset: E,
160
+ recalculate: R
161
+ } = he(g);
162
+ return le(
163
+ s,
164
+ () => ({ scroller: A, render: H, jumpToElement: S, scrollToPercentage: v, reset: E, recalculate: R }),
165
+ [A, H, S, v, E, R]
166
+ ), /* @__PURE__ */ ee(
157
167
  "div",
158
168
  {
159
- ref: N,
169
+ ref: C,
160
170
  className: r,
161
- "data-testid": d,
162
- style: { position: "relative", overflow: "hidden", ...i },
171
+ "data-testid": h,
172
+ style: { position: "relative", overflow: "hidden", ...f },
163
173
  children: [
164
- o,
165
- P
174
+ c,
175
+ M
166
176
  ]
167
177
  }
168
178
  );
169
179
  }
170
- const pe = ee(fe);
180
+ const we = oe(ge);
171
181
  export {
172
- pe as CeriousScroll,
173
- we as CeriousScrollEngine,
174
- ae as useCeriousScroll
182
+ we as CeriousScroll,
183
+ ye as CeriousScrollEngine,
184
+ he as useCeriousScroll
175
185
  };
@@ -10,6 +10,12 @@ export interface UseCeriousScrollOptions<TItem = unknown> {
10
10
  getItem?: (index: number) => TItem;
11
11
  /** Renders a single row. `item` is `undefined` when no data source is given. */
12
12
  renderItem: (item: TItem, index: number) => ReactNode;
13
+ /**
14
+ * Table mode only: declarative header content rendered into the engine's
15
+ * `<thead>` (provide a `<tr>` of `<th>`s). It lives in the same `<table>` as
16
+ * the rows, so columns align natively. Reactive — re-renders like any portal.
17
+ */
18
+ tableHeader?: ReactNode;
13
19
  /** Options forwarded to `new CeriousScroll(...)` (read once, at creation). */
14
20
  options?: CeriousScrollOptions;
15
21
  /** Automatically render after scroll/resize/data changes. Default: `true`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceriousdevtech/react-cerious-scroll",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "React 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",
@@ -56,7 +56,7 @@
56
56
  "demo:build": "vite build --config demo/vite.config.ts"
57
57
  },
58
58
  "dependencies": {
59
- "@ceriousdevtech/cerious-scroll": "^1.0.5"
59
+ "@ceriousdevtech/cerious-scroll": "^1.0.6"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "react": ">=18 <20",