@ceriousdevtech/react-cerious-scroll 1.0.3 → 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,20 @@ 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
+
17
+ ## [1.0.4] - 2026-06-04
18
+
19
+ ### Dependencies
20
+ - Bumped `@ceriousdevtech/cerious-scroll` to `^1.0.5`. Consumers get the new wheel input classifier (trackpad / free-scroll mice apply input immediately, ratcheted wheel notches still ease smoothly), the new `wheel.wheelBehavior` option (`'auto' | 'immediate' | 'smooth'`), and a fix for horizontal wheel forwarding in layouts where `overflow-x: auto` lives on an ancestor of `[data-cerious-scroll-content]`.
21
+
8
22
  ## [1.0.3] - 2026-06-03
9
23
 
10
24
  ### 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 z=require("react/jsx-runtime"),r=require("react"),H=require("react-dom"),G=require("react-dom/server"),_=require("@ceriousdevtech/cerious-scroll"),V="data-cerious-scroll-content",J="data-cerious-scroll-row";function K(e){const l=e.querySelector(`[${V}]`);if(l)return l;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,l){const n=u=>{const o=u.detail;o&&l({percentage:o.percentage,currentElement:o.currentElement,scrollOffset:o.scrollOffset,result:{element:o.result.element,offset:o.result.offset}})},a=u=>{const o=u.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",a),()=>{e.removeEventListener("cerious-viewport-change",n),e.removeEventListener("viewport-change",a)}}function U(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 D(e){var N;const l=r.useRef(null),n=r.useRef(null),a=r.useRef(new Map),u=r.useRef(null),[,o]=r.useReducer(t=>t+1,0),[q,O]=r.useState(null),S=r.useRef(e.renderItem),d=r.useRef(e.items??null),v=r.useRef(e.getItem),p=r.useRef(e.options),h=r.useRef(e.autoRender??!0),b=r.useRef(e.totalElements??null),g=r.useRef(e.onViewportChange),T=r.useRef(e.onMeasuredViewport),I=r.useRef(e.onReady);S.current=e.renderItem,d.current=e.items??null,v.current=e.getItem,p.current=e.options,h.current=e.autoRender??!0,b.current=e.totalElements??null,g.current=e.onViewportChange,T.current=e.onMeasuredViewport,I.current=e.onReady;const k=r.useCallback(t=>{const c=v.current;if(c)return c(t);const R=d.current;return R?R[t]:void 0},[]),s=r.useCallback(()=>{var y;const t=n.current;if(!t)return null;const{scroller:c,contentEl:R,container:C}=t,j=C.clientHeight||C.offsetHeight||0,w=[],i=(f,E)=>{const m=document.createElement("div");m.setAttribute(J,String(f)),m.innerHTML=G.renderToStaticMarkup(r.createElement(r.Fragment,null,S.current(k(f),f))),E.appendChild(m),a.current.set(f,{el:E,mount:m}),w.push(m)},A=c.renderViewport(j,R,i),M=new Set(c.getRenderedIndices());return a.current.forEach((f,E)=>{M.has(E)||a.current.delete(E)}),w.forEach(f=>{f.textContent=""}),H.flushSync(o),(y=T.current)==null||y.call(T,A),A},[]),B=e.totalElements??((N=e.items)==null?void 0:N.length)??null;r.useEffect(()=>{var y,f;const t=l.current;if(!t)return;const c=K(t),R=p.current??{},C=R.onScroll,j={...R,onScroll:()=>{C==null||C(),h.current&&s()}},w=U(b.current,((y=d.current)==null?void 0:y.length)??null),i=new _.CeriousScroll(t,w,j);u.current&&(i.currentElement=Math.min(u.current.currentElement,w-1),i.scrollOffset=u.current.scrollOffset,u.current=null),n.current={scroller:i,contentEl:c,container:t},O(i);const A=Q(t,E=>{var m;(m=g.current)==null||m.call(g,E)});(f=I.current)==null||f.call(I,i);let M=0;return h.current&&(M=requestAnimationFrame(()=>s())),()=>{cancelAnimationFrame(M),A(),u.current={currentElement:i.currentElement,scrollOffset:i.scrollOffset},a.current.clear(),c.textContent="",i.detachScrollbar(t),i.dispose(),n.current=null,O(null)}},[B,s]),r.useEffect(()=>{if(!n.current||!h.current)return;const t=requestAnimationFrame(()=>s());return()=>cancelAnimationFrame(t)},[e.items,e.getItem]);const W=r.useCallback(t=>{const c=n.current;return c?(c.scroller.jumpToElement(t),s()):null},[s]),X=r.useCallback(t=>{const c=n.current;return c?(c.scroller.handleScrollPercentage(t),s()):null},[s]),Y=r.useCallback(()=>{const t=n.current;return t?(t.scroller.reset(),s()):null},[s]),$=r.useCallback(()=>{const t=n.current;return t?(t.scroller.clearAllCaches(),s()):null},[s]),P=d.current,F=!v.current&&P!=null?P.length:null,L=[];return a.current.forEach((t,c)=>{F!==null&&c>=F||L.push(H.createPortal(S.current(k(c),c),t.mount,String(c)))}),{containerRef:l,portals:L,scroller:q,render:s,jumpToElement:W,scrollToPercentage:X,reset:Y,recalculate:$}}function Z(e,l){const{className:n,style:a,"data-testid":u,children:o,...q}=e,{containerRef:O,portals:S,scroller:d,render:v,jumpToElement:p,scrollToPercentage:h,reset:b,recalculate:g}=D(q);return r.useImperativeHandle(l,()=>({scroller:d,render:v,jumpToElement:p,scrollToPercentage:h,reset:b,recalculate:g}),[d,v,p,h,b,g]),z.jsxs("div",{ref:O,className:n,"data-testid":u,style:{position:"relative",overflow:"hidden",...a},children:[o,S]})}const x=r.forwardRef(Z);Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>_.CeriousScroll});exports.CeriousScroll=x;exports.useCeriousScroll=D;
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,163 +1,185 @@
1
- import { jsxs as Y } from "react/jsx-runtime";
2
- import { useRef as s, useReducer as z, useState as G, useCallback as R, useEffect as _, createElement as J, Fragment as K, forwardRef as Q, useImperativeHandle as U } from "react";
3
- import { flushSync as Z, createPortal as x } from "react-dom";
4
- import { renderToStaticMarkup as ee } from "react-dom/server";
5
- import { CeriousScroll as te } from "@ceriousdevtech/cerious-scroll";
6
- import { CeriousScroll as Ee } from "@ceriousdevtech/cerious-scroll";
7
- const k = "data-cerious-scroll-content", re = "data-cerious-scroll-row";
8
- function ne(e) {
9
- const c = e.querySelector(`[${k}]`);
10
- if (c) return c;
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(k, ""), 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 oe(e, c) {
15
- const r = (u) => {
16
- const o = u.detail;
17
- o && c({
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
- }, a = (u) => {
24
- const o = u.detail;
25
- o && c({
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", a), () => {
33
- e.removeEventListener("cerious-viewport-change", r), e.removeEventListener("viewport-change", a);
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 ce(e, c) {
37
- const r = typeof e == "number" ? e : typeof c == "number" ? c : 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 le(e) {
43
- var q;
44
- const c = s(null), r = s(null), a = s(/* @__PURE__ */ new Map()), u = s(null), [, o] = z((t) => t + 1, 0), [L, O] = G(null), S = s(e.renderItem), d = s(e.items ?? null), v = s(e.getItem), C = s(e.options), h = s(e.autoRender ?? !0), T = s(e.totalElements ?? null), g = s(e.onViewportChange), b = s(e.onMeasuredViewport), A = s(e.onReady);
45
- S.current = e.renderItem, d.current = e.items ?? null, v.current = e.getItem, C.current = e.options, h.current = e.autoRender ?? !0, T.current = e.totalElements ?? null, g.current = e.onViewportChange, b.current = e.onMeasuredViewport, A.current = e.onReady;
46
- const P = R((t) => {
47
- const n = v.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 E = d.current;
50
- return E ? E[t] : void 0;
51
- }, []), l = R(() => {
52
- var I;
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: E, container: w } = t, N = w.clientHeight || w.offsetHeight || 0, y = [], i = (f, p) => {
56
- const m = document.createElement("div");
57
- m.setAttribute(re, String(f)), m.innerHTML = ee(
58
- J(K, null, S.current(P(f), f))
59
- ), p.appendChild(m), a.current.set(f, { el: p, mount: m }), y.push(m);
60
- }, M = n.renderViewport(N, E, i), F = new Set(n.getRenderedIndices());
61
- return a.current.forEach((f, p) => {
62
- F.has(p) || a.current.delete(p);
63
- }), y.forEach((f) => {
64
- f.textContent = "";
65
- }), Z(o), (I = b.current) == null || I.call(b, M), M;
66
- }, []), $ = e.totalElements ?? ((q = e.items) == null ? void 0 : q.length) ?? null;
67
- _(() => {
68
- var I, f;
69
- const t = c.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 = ne(t), E = C.current ?? {}, w = E.onScroll, N = {
72
- ...E,
71
+ const n = fe(t), l = _.current ?? {}, a = l.onScroll, j = {
72
+ ...l,
73
73
  onScroll: () => {
74
- w == null || w(), h.current && l();
74
+ a == null || a(), F.current && u();
75
75
  }
76
- }, y = ce(T.current, ((I = d.current) == null ? void 0 : I.length) ?? null), i = new te(t, y, N);
77
- u.current && (i.currentElement = Math.min(u.current.currentElement, y - 1), i.scrollOffset = u.current.scrollOffset, u.current = null), r.current = { scroller: i, contentEl: n, container: t }, O(i);
78
- const M = oe(t, (p) => {
79
- var m;
80
- (m = g.current) == null || m.call(g, p);
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
- (f = A.current) == null || f.call(A, i);
83
- let F = 0;
84
- return h.current && (F = requestAnimationFrame(() => l())), () => {
85
- cancelAnimationFrame(F), M(), u.current = {
86
- currentElement: i.currentElement,
87
- scrollOffset: i.scrollOffset
88
- }, a.current.clear(), n.textContent = "", i.detachScrollbar(t), i.dispose(), r.current = null, O(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
- }, [$, l]), _(() => {
91
- if (!r.current || !h.current) return;
92
- const t = requestAnimationFrame(() => l());
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 B = R(
105
+ const Q = w(
96
106
  (t) => {
97
107
  const n = r.current;
98
- return n ? (n.scroller.jumpToElement(t), l()) : null;
108
+ return n ? (n.scroller.jumpToElement(t), u()) : null;
99
109
  },
100
- [l]
101
- ), D = R(
110
+ [u]
111
+ ), U = w(
102
112
  (t) => {
103
113
  const n = r.current;
104
- return n ? (n.scroller.handleScrollPercentage(t), l()) : null;
114
+ return n ? (n.scroller.handleScrollPercentage(t), u()) : null;
105
115
  },
106
- [l]
107
- ), W = R(() => {
116
+ [u]
117
+ ), Z = w(() => {
108
118
  const t = r.current;
109
- return t ? (t.scroller.reset(), l()) : null;
110
- }, [l]), X = R(() => {
119
+ return t ? (t.scroller.reset(), u()) : null;
120
+ }, [u]), x = w(() => {
111
121
  const t = r.current;
112
- return t ? (t.scroller.clearAllCaches(), l()) : null;
113
- }, [l]), j = d.current, H = !v.current && j != null ? j.length : null, V = [];
114
- return a.current.forEach((t, n) => {
115
- H !== null && n >= H || V.push(
116
- x(S.current(P(n), n), t.mount, String(n))
117
- );
118
- }), {
119
- containerRef: c,
120
- portals: V,
121
- scroller: L,
122
- render: l,
123
- jumpToElement: B,
124
- scrollToPercentage: D,
125
- reset: W,
126
- recalculate: X
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);
129
+ return;
130
+ }
131
+ const l = B(n);
132
+ let a = p.get(n);
133
+ (!a || a.item !== l || a.mount !== t.mount) && (a = {
134
+ item: l,
135
+ mount: t.mount,
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
127
149
  };
128
150
  }
129
- function se(e, c) {
130
- const { className: r, style: a, "data-testid": u, children: o, ...L } = e, {
131
- containerRef: O,
132
- portals: S,
133
- scroller: d,
134
- render: v,
135
- jumpToElement: C,
136
- scrollToPercentage: h,
137
- reset: T,
138
- recalculate: g
139
- } = le(L);
140
- return U(
141
- c,
142
- () => ({ scroller: d, render: v, jumpToElement: C, scrollToPercentage: h, reset: T, recalculate: g }),
143
- [d, v, C, h, T, g]
144
- ), /* @__PURE__ */ Y(
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(
145
167
  "div",
146
168
  {
147
- ref: O,
169
+ ref: C,
148
170
  className: r,
149
- "data-testid": u,
150
- style: { position: "relative", overflow: "hidden", ...a },
171
+ "data-testid": h,
172
+ style: { position: "relative", overflow: "hidden", ...f },
151
173
  children: [
152
- o,
153
- S
174
+ c,
175
+ M
154
176
  ]
155
177
  }
156
178
  );
157
179
  }
158
- const de = Q(se);
180
+ const we = oe(ge);
159
181
  export {
160
- de as CeriousScroll,
161
- Ee as CeriousScrollEngine,
162
- le as useCeriousScroll
182
+ we as CeriousScroll,
183
+ ye as CeriousScrollEngine,
184
+ he as useCeriousScroll
163
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.3",
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.4"
59
+ "@ceriousdevtech/cerious-scroll": "^1.0.6"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "react": ">=18 <20",