@ceriousdevtech/react-cerious-scroll 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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.1.0] - 2026-08-22
9
+
10
+ ### Added
11
+ - Declarative canonical and dynamic Masonry support. The wrapper supplies the core DOM `renderItem` callback while consumers continue returning React nodes from the existing `renderItem` prop.
12
+ - `jumpToItem(index, screenOffset?)` on the hook result and component ref.
13
+ - `CeriousScrollOptions` now accepts Masonry geometry without requiring the engine's imperative DOM callback.
14
+ - Canonical-height and dynamic-height Masonry demo routes and regression tests for both rendering paths.
15
+
16
+ ### Changed
17
+ - Updated `@ceriousdevtech/cerious-scroll` to `^1.1.0` and re-exported the new Masonry and height-provider types.
18
+ - Masonry card-count changes recreate card-derived segment state; list and table count changes continue updating in place.
19
+
8
20
  ## [1.0.7] - 2026-06-24
9
21
 
10
22
  ### Changed
package/README.md CHANGED
@@ -110,7 +110,7 @@ function List() {
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
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
+ | `options` | `CeriousScrollOptions` | Engine options. Masonry's DOM callback is supplied by the wrapper. Read once at creation. |
114
114
  | `autoRender` | `boolean` | Re-render on scroll/resize/data changes. Default `true`. |
115
115
  | `onViewportChange` | `(detail) => void` | Normalized viewport-change callback. |
116
116
  | `onMeasuredViewport` | `(range) => void` | Measured range after each render pass. |
@@ -122,6 +122,7 @@ function List() {
122
122
  ```tsx
123
123
  const ref = useRef<CeriousScrollHandle>(null);
124
124
  // ref.current?.jumpToElement(500);
125
+ // ref.current?.jumpToItem(500); // Masonry cards
125
126
  // ref.current?.scrollToPercentage(50);
126
127
  // ref.current?.reset();
127
128
  // ref.current?.render();
@@ -131,6 +132,38 @@ const ref = useRef<CeriousScrollHandle>(null);
131
132
 
132
133
  ---
133
134
 
135
+ ## Masonry layout
136
+
137
+ Set `layout: 'masonry'` and provide Masonry geometry without a DOM
138
+ `renderItem`; the wrapper connects your existing React render prop to the core
139
+ renderer. Supplying `getItemHeight` selects canonical placement:
140
+
141
+ ```tsx
142
+ <CeriousScroll
143
+ ref={ref}
144
+ className="gallery"
145
+ totalElements={photos.length}
146
+ getItem={(index) => photos[index]}
147
+ options={{
148
+ layout: 'masonry',
149
+ masonry: {
150
+ getItemHeight: (_index, width) => width * 0.75 + 48,
151
+ targetColumnWidth: 280,
152
+ gap: 16,
153
+ },
154
+ }}
155
+ renderItem={(photo, index) => <PhotoCard photo={photo} index={index} />}
156
+ />
157
+ ```
158
+
159
+ Omit `getItemHeight` for dynamic DOM-measured cards. React static markup is
160
+ used for the offscreen measurement probe; visible cards remain normal live
161
+ portals with Context, refs, state, and events.
162
+
163
+ Use `ref.current?.jumpToItem(index, screenOffset?)` for card navigation and
164
+ `ref.current?.scroller?.masonryDeterminism` to read `'canonical'` or `'local'`.
165
+ The demo gallery includes matching canonical and dynamic Masonry pages.
166
+
134
167
  ## Table layout
135
168
 
136
169
  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:
@@ -173,8 +206,8 @@ import { TABLE_COLUMNS } from './data';
173
206
  built-in `ResizeObserver`.
174
207
  - **`options` are read at creation.** Changing `options` after mount has no
175
208
  effect; remount (e.g. with a `key`) to apply new engine options.
176
- - **Changing the item count** recreates the engine internally (scroll position
177
- is preserved). Mutating items without changing the count just re-renders the
209
+ - **Changing the item count** updates lists/tables in place. Masonry recreates
210
+ its card-count-derived segment layout. Mutating items without changing the count just re-renders the
178
211
  content (cheap, and your row state is preserved) — it does **not** discard
179
212
  cached heights, so editable grids that produce a new `items` array on every
180
213
  edit don't trigger a full viewport re-measure.
@@ -24,6 +24,7 @@ export interface CeriousScrollHandle {
24
24
  scroller: CeriousScrollEngine | null;
25
25
  render(): MeasuredViewportRange | null;
26
26
  jumpToElement(index: number): MeasuredViewportRange | null;
27
+ jumpToItem(index: number, screenOffset?: number): MeasuredViewportRange | null;
27
28
  scrollToPercentage(percentage: number): MeasuredViewportRange | null;
28
29
  reset(): MeasuredViewportRange | null;
29
30
  /**
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const te=require("react/jsx-runtime"),r=require("react"),D=require("react-dom"),re=require("react-dom/server"),K=require("@ceriousdevtech/cerious-scroll"),G="data-cerious-scroll-content",ne="data-cerious-scroll-row";function ce(e){const a=e.querySelector(`[${G}]`);if(a)return a;const c=document.createElement("div");return c.setAttribute(G,""),c.style.position="relative",c.style.width="100%",c.style.height="100%",c.style.overflowY="clip",c.style.overflowX="auto",e.appendChild(c),c}function le(e,a){const c=h=>{const l=h.detail;l&&a({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&&a({percentage:l.percentage,currentElement:l.element,scrollOffset:l.scrollOffset,result:{element:l.element,offset:l.scrollOffset}})};return e.addEventListener("cerious-viewport-change",c),e.addEventListener("viewport-change",f),()=>{e.removeEventListener("cerious-viewport-change",c),e.removeEventListener("viewport-change",f)}}function J(e,a){const c=typeof e=="number"?e:typeof a=="number"?a:void 0;if(c===void 0||Number.isNaN(c))throw new Error("useCeriousScroll: provide `totalElements` or `items`.");return Math.max(1,Math.floor(c))}function Q(e){var $;const a=r.useRef(null),c=r.useRef(null),f=r.useRef(new Map),h=r.useRef(new Map),l=r.useRef(null),g=r.useRef(null),S=r.useRef(null),M=r.useRef(e.tableHeader),[,A]=r.useReducer(t=>t+1,0),[q,p]=r.useState(null),b=r.useRef(e.renderItem),R=r.useRef(e.items??null),v=r.useRef(e.getItem),L=r.useRef(e.options),w=r.useRef(e.autoRender??!0),N=r.useRef(e.totalElements??null),j=r.useRef(e.onViewportChange),H=r.useRef(e.onMeasuredViewport),P=r.useRef(e.onReady);b.current=e.renderItem,R.current=e.items??null,v.current=e.getItem,M.current=e.tableHeader,L.current=e.options,w.current=e.autoRender??!0,N.current=e.totalElements??null,j.current=e.onViewportChange,H.current=e.onMeasuredViewport,P.current=e.onReady;const B=r.useCallback(t=>{const n=v.current;if(n)return n(t);const o=R.current;return o?o[t]:void 0},[]),s=r.useCallback(()=>{var I,O;const t=c.current;if(!t)return null;const{scroller:n,contentEl:o,container:u}=t,k=u.clientHeight||u.offsetHeight||0,y=[],m=((I=L.current)==null?void 0:I.layout)==="table",z=(i,d)=>{const C=document.createElement("div");m&&(C.style.display="contents"),C.setAttribute(ne,String(i)),C.innerHTML=re.renderToStaticMarkup(r.createElement(r.Fragment,null,b.current(B(i),i))),d.appendChild(C),f.current.set(i,{el:d,mount:C}),y.push(C)},T=n.renderViewport(k,o,z),F=new Set(n.getRenderedIndices());return f.current.forEach((i,d)=>{F.has(d)||f.current.delete(d)}),y.forEach(i=>{i.textContent=""}),D.flushSync(A),(O=H.current)==null||O.call(H,T),T},[]),W=e.totalElements??(($=e.items)==null?void 0:$.length)??null;r.useEffect(()=>{var F,I,O;const t=a.current;if(!t)return;const n=ce(t),o=L.current??{},u=o.onScroll,k={...o,onScroll:()=>{u==null||u(),w.current&&s()}};if(o.layout==="table"&&M.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 y=J(N.current,((I=R.current)==null?void 0:I.length)??null),m=new K.CeriousScroll(t,y,k);g.current&&(m.currentElement=Math.min(g.current.currentElement,y-1),m.scrollOffset=g.current.scrollOffset,g.current=null),c.current={scroller:m,contentEl:n,container:t},p(m);const z=le(t,i=>{var d;(d=j.current)==null||d.call(j,i)});(O=P.current)==null||O.call(P,m);let T=0;return w.current&&(T=requestAnimationFrame(()=>s())),()=>{cancelAnimationFrame(T),z(),g.current={currentElement:m.currentElement,scrollOffset:m.scrollOffset},f.current.clear(),h.current.clear(),l.current=null,n.textContent="",m.detachScrollbar(t),m.dispose(),c.current=null,p(null)}},[s]),r.useEffect(()=>{var o;const t=c.current;if(!t||W==null)return;const n=J(N.current,((o=R.current)==null?void 0:o.length)??null);if(n!==t.scroller.totalElements)if(t.scroller.updateTotalElements(n),t.scroller.currentElement>n-1&&t.scroller.jumpToElement(n-1),w.current){const u=t.scroller;requestAnimationFrame(()=>{s(),u.syncScrollbar()})}else t.scroller.syncScrollbar()},[W,s]),r.useEffect(()=>{if(!c.current||!w.current)return;const t=requestAnimationFrame(()=>s());return()=>cancelAnimationFrame(t)},[e.items,e.getItem]);const U=r.useCallback(t=>{const n=c.current;return n?(n.scroller.jumpToElement(t),s()):null},[s]),Z=r.useCallback(t=>{const n=c.current;return n?(n.scroller.handleScrollPercentage(t),s()):null},[s]),x=r.useCallback(()=>{const t=c.current;return t?(t.scroller.reset(),s()):null},[s]),ee=r.useCallback(()=>{const t=c.current;return t?(t.scroller.clearAllCaches(),h.current.clear(),s()):null},[s]),X=R.current,Y=!v.current&&X!=null?X.length:null,V=b.current,E=h.current;l.current!==V&&(E.clear(),l.current=V);const _=[];return f.current.forEach((t,n)=>{if(Y!==null&&n>=Y){E.delete(n);return}const o=B(n);let u=E.get(n);(!u||u.item!==o||u.mount!==t.mount)&&(u={item:o,mount:t.mount,node:D.createPortal(V(o,n),t.mount,String(n))},E.set(n,u)),_.push(u.node)}),E.size>f.current.size&&E.forEach((t,n)=>{f.current.has(n)||E.delete(n)}),e.tableHeader!=null&&S.current&&_.push(D.createPortal(e.tableHeader,S.current,"cerious-thead")),{containerRef:a,portals:_,scroller:q,render:s,jumpToElement:U,scrollToPercentage:Z,reset:x,recalculate:ee}}function oe(e,a){const{className:c,style:f,"data-testid":h,children:l,...g}=e,{containerRef:S,portals:M,scroller:A,render:q,jumpToElement:p,scrollToPercentage:b,reset:R,recalculate:v}=Q(g);return r.useImperativeHandle(a,()=>({scroller:A,render:q,jumpToElement:p,scrollToPercentage:b,reset:R,recalculate:v}),[A,q,p,b,R,v]),te.jsxs("div",{ref:S,className:c,"data-testid":h,style:{position:"relative",overflow:"hidden",...f},children:[l,M]})}const se=r.forwardRef(oe);Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>K.CeriousScroll});exports.CeriousScroll=se;exports.useCeriousScroll=Q;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const ae=require("react/jsx-runtime"),n=require("react"),G=require("react-dom"),x=require("react-dom/server"),re=require("@ceriousdevtech/cerious-scroll"),ee="data-cerious-scroll-content",_="data-cerious-scroll-row";function ie(r){const m=r.querySelector(`[${ee}]`);if(m)return m;const c=document.createElement("div");return c.setAttribute(ee,""),c.style.position="relative",c.style.width="100%",c.style.height="100%",c.style.overflowY="clip",c.style.overflowX="auto",r.appendChild(c),c}function fe(r,m){const c=E=>{const l=E.detail;l&&m({percentage:l.percentage,currentElement:l.currentElement,scrollOffset:l.scrollOffset,result:{element:l.result.element,offset:l.result.offset}})},a=E=>{const l=E.detail;l&&m({percentage:l.percentage,currentElement:l.element,scrollOffset:l.scrollOffset,result:{element:l.element,offset:l.scrollOffset}})};return r.addEventListener("cerious-viewport-change",c),r.addEventListener("viewport-change",a),()=>{r.removeEventListener("cerious-viewport-change",c),r.removeEventListener("viewport-change",a)}}function te(r,m){const c=typeof r=="number"?r:typeof m=="number"?m:void 0;if(c===void 0||Number.isNaN(c))throw new Error("useCeriousScroll: provide `totalElements` or `items`.");return Math.max(1,Math.floor(c))}function ne(r){var Z;const m=n.useRef(null),c=n.useRef(null),a=n.useRef(new Map),E=n.useRef(new Map),l=n.useRef(null),y=n.useRef(null),w=n.useRef(null),q=n.useRef(r.tableHeader),[,k]=n.useReducer(e=>e+1,0),[H,T]=n.useState(null),[F,P]=n.useReducer(e=>e+1,0),p=n.useRef(r.renderItem),b=n.useRef(r.items??null),$=n.useRef(r.getItem),S=n.useRef(r.options),I=n.useRef(r.autoRender??!0),B=n.useRef(r.totalElements??null),L=n.useRef(r.onViewportChange),N=n.useRef(r.onMeasuredViewport),V=n.useRef(r.onReady);p.current=r.renderItem,b.current=r.items??null,$.current=r.getItem,q.current=r.tableHeader,S.current=r.options,I.current=r.autoRender??!0,B.current=r.totalElements??null,L.current=r.onViewportChange,N.current=r.onMeasuredViewport,V.current=r.onReady;const z=n.useCallback(e=>{const t=$.current;if(t)return t(e);const s=b.current;return s?s[e]:void 0},[]),J=n.useCallback((e,t)=>{const s=z(e);if(t.dataset.ceriousMasonry==="probe"){const f=document.createElement("div");f.setAttribute(_,String(e)),f.innerHTML=x.renderToStaticMarkup(n.createElement(n.Fragment,null,p.current(s,e))),t.appendChild(f);return}let o=t.querySelector(`[${_}]`);o||(o=document.createElement("div"),t.appendChild(o)),o.setAttribute(_,String(e)),a.current.forEach((f,R)=>{f.el===t&&R!==e&&a.current.delete(R)}),a.current.set(e,{el:t,mount:o})},[z]),u=n.useCallback(()=>{var O,A,j;const e=c.current;if(!e)return null;const{scroller:t,contentEl:s,container:o}=e,f=o.clientHeight||o.offsetHeight||0,R=[],M=((O=S.current)==null?void 0:O.layout)==="table",v=((A=S.current)==null?void 0:A.layout)==="masonry",D=(h,i)=>{const g=document.createElement("div");M&&(g.style.display="contents"),g.setAttribute(_,String(h)),g.innerHTML=x.renderToStaticMarkup(n.createElement(n.Fragment,null,p.current(z(h),h))),i.appendChild(g),a.current.set(h,{el:i,mount:g}),R.push(g)},d=t.renderViewport(f,s,D),Y=v?null:new Set(t.getRenderedIndices());return a.current.forEach((h,i)=>{v?(!h.el.isConnected||h.el.dataset.elementIndex!==String(i))&&a.current.delete(i):Y.has(i)||a.current.delete(i)}),R.forEach(h=>{h.textContent=""}),G.flushSync(k),(j=N.current)==null||j.call(N,d),d},[]),K=r.totalElements??((Z=r.items)==null?void 0:Z.length)??null;n.useEffect(()=>{var A,j,h;const e=m.current;if(!e)return;const t=S.current??{},s=t.layout==="masonry",o=s?e:ie(e),f=t.onScroll,{masonry:R,...M}=t,v={...M,onScroll:()=>{f==null||f(),I.current&&u()}};if(s){if(!R)throw new Error("useCeriousScroll: layout 'masonry' requires `options.masonry`.");v.masonry={...R,renderItem:J}}if(t.layout==="table"&&q.current!=null){const i=(A=t.table)==null?void 0:A.header;v.table={...t.table,header:g=>{w.current=g,i==null||i(g)}}}const D=te(B.current,((j=b.current)==null?void 0:j.length)??null),d=new re.CeriousScroll(e,D,v);y.current&&(d.currentElement=Math.min(y.current.currentElement,D-1),d.scrollOffset=y.current.scrollOffset,y.current=null),c.current={scroller:d,contentEl:o,container:e},T(d);const Y=fe(e,i=>{var g;(g=L.current)==null||g.call(L,i)});(h=V.current)==null||h.call(V,d);let O=0;return I.current&&(O=requestAnimationFrame(()=>u())),()=>{var i;cancelAnimationFrame(O),Y(),y.current=((i=S.current)==null?void 0:i.layout)==="masonry"?null:{currentElement:d.currentElement,scrollOffset:d.scrollOffset},a.current.clear(),E.current.clear(),l.current=null,d.detachScrollbar(e),d.dispose(),o.textContent="",c.current=null,T(null)}},[u,J,F]),n.useEffect(()=>{var f,R;const e=c.current;if(!e||K==null)return;const t=te(B.current,((f=b.current)==null?void 0:f.length)??null),s=((R=S.current)==null?void 0:R.layout)==="masonry",o=s?e.scroller.itemCount:e.scroller.totalElements;if(t!==o){if(s){P();return}if(e.scroller.updateTotalElements(t),e.scroller.currentElement>t-1&&e.scroller.jumpToElement(t-1),I.current){const M=e.scroller;requestAnimationFrame(()=>{u(),M.syncScrollbar()})}else e.scroller.syncScrollbar()}},[K,u]),n.useEffect(()=>{if(!c.current||!I.current)return;const e=requestAnimationFrame(()=>u());return()=>cancelAnimationFrame(e)},[r.items,r.getItem]);const ce=n.useCallback(e=>{const t=c.current;return t?(t.scroller.jumpToElement(e),u()):null},[u]),oe=n.useCallback((e,t=0)=>{const s=c.current;return s?(s.scroller.jumpToItem(e,t),u()):null},[u]),le=n.useCallback(e=>{const t=c.current;return t?(t.scroller.handleScrollPercentage(e),u()):null},[u]),se=n.useCallback(()=>{const e=c.current;return e?(e.scroller.reset(),u()):null},[u]),ue=n.useCallback(()=>{const e=c.current;return e?(e.scroller.clearAllCaches(),E.current.clear(),u()):null},[u]),Q=b.current,U=!$.current&&Q!=null?Q.length:null,W=p.current,C=E.current;l.current!==W&&(C.clear(),l.current=W);const X=[];return a.current.forEach((e,t)=>{if(U!==null&&t>=U){C.delete(t);return}const s=z(t);let o=C.get(t);(!o||o.item!==s||o.mount!==e.mount)&&(o={item:s,mount:e.mount,node:G.createPortal(W(s,t),e.mount,String(t))},C.set(t,o)),X.push(o.node)}),C.size>a.current.size&&C.forEach((e,t)=>{a.current.has(t)||C.delete(t)}),r.tableHeader!=null&&w.current&&X.push(G.createPortal(r.tableHeader,w.current,"cerious-thead")),{containerRef:m,portals:X,scroller:H,render:u,jumpToElement:ce,jumpToItem:oe,scrollToPercentage:le,reset:se,recalculate:ue}}function me(r,m){const{className:c,style:a,"data-testid":E,children:l,...y}=r,{containerRef:w,portals:q,scroller:k,render:H,jumpToElement:T,jumpToItem:F,scrollToPercentage:P,reset:p,recalculate:b}=ne(y);return n.useImperativeHandle(m,()=>({scroller:k,render:H,jumpToElement:T,jumpToItem:F,scrollToPercentage:P,reset:p,recalculate:b}),[k,H,T,F,P,p,b]),ae.jsxs("div",{ref:w,className:c,"data-testid":E,style:{position:"relative",overflow:"hidden",...a},children:[l,q]})}const de=n.forwardRef(me);Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>re.CeriousScroll});exports.CeriousScroll=de;exports.useCeriousScroll=ne;
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  export { CeriousScroll } from './cerious-scroll';
9
9
  export type { CeriousScrollProps, CeriousScrollHandle } from './cerious-scroll';
10
10
  export { useCeriousScroll } from './use-cerious-scroll';
11
- export type { UseCeriousScrollOptions, UseCeriousScrollResult, } from './use-cerious-scroll';
11
+ export type { UseCeriousScrollOptions, UseCeriousScrollResult, CeriousScrollOptions, } from './use-cerious-scroll';
12
12
  export type { CeriousViewportChangeDetail } from './viewport-change';
13
13
  export { CeriousScroll as CeriousScrollEngine } from '@ceriousdevtech/cerious-scroll';
14
- export type { CeriousScrollOptions, KeyboardNavigationOptions, TouchNavigationOptions, WheelNavigationOptions, ElementRenderer, ElementHeightCalculator, ScrollResult, MeasuredViewportRange, } from '@ceriousdevtech/cerious-scroll';
14
+ export type { KeyboardNavigationOptions, TouchNavigationOptions, WheelNavigationOptions, ElementRenderer, ElementHeightCalculator, ScrollResult, MeasuredViewportRange, HeightProvider, MasonryDeterminism, MasonryOptions, } from '@ceriousdevtech/cerious-scroll';
package/dist/index.js CHANGED
@@ -1,198 +1,230 @@
1
- import { jsxs as te } from "react/jsx-runtime";
2
- import { useRef as o, useReducer as re, useState as ne, useCallback as S, useEffect as B, createElement as ce, Fragment as le, forwardRef as oe, useImperativeHandle as se } from "react";
3
- import { flushSync as ue, createPortal as J } from "react-dom";
4
- import { renderToStaticMarkup as ae } from "react-dom/server";
5
- import { CeriousScroll as ie } from "@ceriousdevtech/cerious-scroll";
6
- import { CeriousScroll as ye } from "@ceriousdevtech/cerious-scroll";
7
- const K = "data-cerious-scroll-content", fe = "data-cerious-scroll-row";
8
- function me(e) {
9
- const a = e.querySelector(`[${K}]`);
10
- if (a) return a;
1
+ import { jsxs as fe } from "react/jsx-runtime";
2
+ import { useRef as u, useReducer as ee, useState as de, useCallback as b, createElement as te, Fragment as re, useEffect as J, forwardRef as he, useImperativeHandle as ge } from "react";
3
+ import { flushSync as Ee, createPortal as ne } from "react-dom";
4
+ import { renderToStaticMarkup as oe } from "react-dom/server";
5
+ import { CeriousScroll as pe } from "@ceriousdevtech/cerious-scroll";
6
+ import { CeriousScroll as He } from "@ceriousdevtech/cerious-scroll";
7
+ const ce = "data-cerious-scroll-content", B = "data-cerious-scroll-row";
8
+ function ye(r) {
9
+ const f = r.querySelector(`[${ce}]`);
10
+ if (f) return f;
11
11
  const n = document.createElement("div");
12
- return n.setAttribute(K, ""), n.style.position = "relative", n.style.width = "100%", n.style.height = "100%", n.style.overflowY = "clip", n.style.overflowX = "auto", e.appendChild(n), n;
12
+ return n.setAttribute(ce, ""), n.style.position = "relative", n.style.width = "100%", n.style.height = "100%", n.style.overflowY = "clip", n.style.overflowX = "auto", r.appendChild(n), n;
13
13
  }
14
- function de(e, a) {
15
- const n = (h) => {
16
- const c = h.detail;
17
- c && a({
14
+ function be(r, f) {
15
+ const n = (p) => {
16
+ const c = p.detail;
17
+ c && f({
18
18
  percentage: c.percentage,
19
19
  currentElement: c.currentElement,
20
20
  scrollOffset: c.scrollOffset,
21
21
  result: { element: c.result.element, offset: c.result.offset }
22
22
  });
23
- }, f = (h) => {
24
- const c = h.detail;
25
- c && a({
23
+ }, a = (p) => {
24
+ const c = p.detail;
25
+ c && f({
26
26
  percentage: c.percentage,
27
27
  currentElement: c.element,
28
28
  scrollOffset: c.scrollOffset,
29
29
  result: { element: c.element, offset: c.scrollOffset }
30
30
  });
31
31
  };
32
- return e.addEventListener("cerious-viewport-change", n), e.addEventListener("viewport-change", f), () => {
33
- e.removeEventListener("cerious-viewport-change", n), e.removeEventListener("viewport-change", f);
32
+ return r.addEventListener("cerious-viewport-change", n), r.addEventListener("viewport-change", a), () => {
33
+ r.removeEventListener("cerious-viewport-change", n), r.removeEventListener("viewport-change", a);
34
34
  };
35
35
  }
36
- function Q(e, a) {
37
- const n = typeof e == "number" ? e : typeof a == "number" ? a : void 0;
36
+ function le(r, f) {
37
+ const n = typeof r == "number" ? r : typeof f == "number" ? f : void 0;
38
38
  if (n === void 0 || Number.isNaN(n))
39
39
  throw new Error("useCeriousScroll: provide `totalElements` or `items`.");
40
40
  return Math.max(1, Math.floor(n));
41
41
  }
42
- function he(e) {
43
- var G;
44
- const a = o(null), n = o(null), f = o(/* @__PURE__ */ new Map()), h = o(/* @__PURE__ */ new Map()), c = o(null), E = o(null), w = o(null), M = o(e.tableHeader), [, H] = re((t) => t + 1, 0), [F, C] = ne(null), b = o(e.renderItem), g = o(e.items ?? null), v = o(e.getItem), V = o(e.options), y = o(e.autoRender ?? !0), _ = o(e.totalElements ?? null), L = o(e.onViewportChange), N = o(e.onMeasuredViewport), j = o(e.onReady);
45
- b.current = e.renderItem, g.current = e.items ?? null, v.current = e.getItem, M.current = e.tableHeader, V.current = e.options, y.current = e.autoRender ?? !0, _.current = e.totalElements ?? null, L.current = e.onViewportChange, N.current = e.onMeasuredViewport, j.current = e.onReady;
46
- const D = S((t) => {
47
- const r = v.current;
48
- if (r) return r(t);
49
- const l = g.current;
50
- return l ? l[t] : void 0;
51
- }, []), s = S(() => {
52
- var O, A;
53
- const t = n.current;
54
- if (!t) return null;
55
- const { scroller: r, contentEl: l, container: u } = t, P = u.clientHeight || u.offsetHeight || 0, T = [], m = ((O = V.current) == null ? void 0 : O.layout) === "table", $ = (i, d) => {
56
- const R = document.createElement("div");
57
- m && (R.style.display = "contents"), R.setAttribute(fe, String(i)), R.innerHTML = ae(
58
- ce(le, null, b.current(D(i), i))
59
- ), d.appendChild(R), f.current.set(i, { el: d, mount: R }), T.push(R);
60
- }, I = r.renderViewport(P, l, $), q = new Set(r.getRenderedIndices());
61
- return f.current.forEach((i, d) => {
62
- q.has(d) || f.current.delete(d);
63
- }), T.forEach((i) => {
64
- i.textContent = "";
65
- }), ue(H), (A = N.current) == null || A.call(N, I), I;
66
- }, []), W = e.totalElements ?? ((G = e.items) == null ? void 0 : G.length) ?? null;
67
- B(() => {
68
- var q, O, A;
69
- const t = a.current;
70
- if (!t) return;
71
- const r = me(t), l = V.current ?? {}, u = l.onScroll, P = {
72
- ...l,
42
+ function ve(r) {
43
+ var x;
44
+ const f = u(null), n = u(null), a = u(/* @__PURE__ */ new Map()), p = u(/* @__PURE__ */ new Map()), c = u(null), v = u(null), I = u(null), F = u(r.tableHeader), [, L] = ee((e) => e + 1, 0), [q, T] = de(null), [N, P] = ee((e) => e + 1, 0), S = u(r.renderItem), y = u(r.items ?? null), D = u(r.getItem), R = u(r.options), M = u(r.autoRender ?? !0), W = u(r.totalElements ?? null), V = u(r.onViewportChange), $ = u(r.onMeasuredViewport), k = u(r.onReady);
45
+ S.current = r.renderItem, y.current = r.items ?? null, D.current = r.getItem, F.current = r.tableHeader, R.current = r.options, M.current = r.autoRender ?? !0, W.current = r.totalElements ?? null, V.current = r.onViewportChange, $.current = r.onMeasuredViewport, k.current = r.onReady;
46
+ const z = b((e) => {
47
+ const t = D.current;
48
+ if (t) return t(e);
49
+ const l = y.current;
50
+ return l ? l[e] : void 0;
51
+ }, []), K = b((e, t) => {
52
+ const l = z(e);
53
+ if (t.dataset.ceriousMasonry === "probe") {
54
+ const m = document.createElement("div");
55
+ m.setAttribute(B, String(e)), m.innerHTML = oe(
56
+ te(re, null, S.current(l, e))
57
+ ), t.appendChild(m);
58
+ return;
59
+ }
60
+ let o = t.querySelector(`[${B}]`);
61
+ o || (o = document.createElement("div"), t.appendChild(o)), o.setAttribute(B, String(e)), a.current.forEach((m, E) => {
62
+ m.el === t && E !== e && a.current.delete(E);
63
+ }), a.current.set(e, { el: t, mount: o });
64
+ }, [z]), s = b(() => {
65
+ var A, H, j;
66
+ const e = n.current;
67
+ if (!e) return null;
68
+ const { scroller: t, contentEl: l, container: o } = e, m = o.clientHeight || o.offsetHeight || 0, E = [], O = ((A = R.current) == null ? void 0 : A.layout) === "table", w = ((H = R.current) == null ? void 0 : H.layout) === "masonry", _ = (h, i) => {
69
+ const g = document.createElement("div");
70
+ O && (g.style.display = "contents"), g.setAttribute(B, String(h)), g.innerHTML = oe(
71
+ te(re, null, S.current(z(h), h))
72
+ ), i.appendChild(g), a.current.set(h, { el: i, mount: g }), E.push(g);
73
+ }, d = t.renderViewport(m, l, _), G = w ? null : new Set(t.getRenderedIndices());
74
+ return a.current.forEach((h, i) => {
75
+ w ? (!h.el.isConnected || h.el.dataset.elementIndex !== String(i)) && a.current.delete(i) : G.has(i) || a.current.delete(i);
76
+ }), E.forEach((h) => {
77
+ h.textContent = "";
78
+ }), Ee(L), (j = $.current) == null || j.call($, d), d;
79
+ }, []), Q = r.totalElements ?? ((x = r.items) == null ? void 0 : x.length) ?? null;
80
+ J(() => {
81
+ var H, j, h;
82
+ const e = f.current;
83
+ if (!e) return;
84
+ const t = R.current ?? {}, l = t.layout === "masonry", o = l ? e : ye(e), m = t.onScroll, { masonry: E, ...O } = t, w = {
85
+ ...O,
73
86
  onScroll: () => {
74
- u == null || u(), y.current && s();
87
+ m == null || m(), M.current && s();
75
88
  }
76
89
  };
77
- if (l.layout === "table" && M.current != null) {
78
- const i = (q = l.table) == null ? void 0 : q.header;
79
- P.table = {
80
- ...l.table,
81
- header: (d) => {
82
- w.current = d, i == null || i(d);
90
+ if (l) {
91
+ if (!E)
92
+ throw new Error("useCeriousScroll: layout 'masonry' requires `options.masonry`.");
93
+ w.masonry = {
94
+ ...E,
95
+ renderItem: K
96
+ };
97
+ }
98
+ if (t.layout === "table" && F.current != null) {
99
+ const i = (H = t.table) == null ? void 0 : H.header;
100
+ w.table = {
101
+ ...t.table,
102
+ header: (g) => {
103
+ I.current = g, i == null || i(g);
83
104
  }
84
105
  };
85
106
  }
86
- const T = Q(_.current, ((O = g.current) == null ? void 0 : O.length) ?? null), m = new ie(t, T, P);
87
- E.current && (m.currentElement = Math.min(E.current.currentElement, T - 1), m.scrollOffset = E.current.scrollOffset, E.current = null), n.current = { scroller: m, contentEl: r, container: t }, C(m);
88
- const $ = de(t, (i) => {
89
- var d;
90
- (d = L.current) == null || d.call(L, i);
107
+ const _ = le(W.current, ((j = y.current) == null ? void 0 : j.length) ?? null), d = new pe(e, _, w);
108
+ v.current && (d.currentElement = Math.min(v.current.currentElement, _ - 1), d.scrollOffset = v.current.scrollOffset, v.current = null), n.current = { scroller: d, contentEl: o, container: e }, T(d);
109
+ const G = be(e, (i) => {
110
+ var g;
111
+ (g = V.current) == null || g.call(V, i);
91
112
  });
92
- (A = j.current) == null || A.call(j, m);
93
- let I = 0;
94
- return y.current && (I = requestAnimationFrame(() => s())), () => {
95
- cancelAnimationFrame(I), $(), E.current = {
96
- currentElement: m.currentElement,
97
- scrollOffset: m.scrollOffset
98
- }, f.current.clear(), h.current.clear(), c.current = null, r.textContent = "", m.detachScrollbar(t), m.dispose(), n.current = null, C(null);
113
+ (h = k.current) == null || h.call(k, d);
114
+ let A = 0;
115
+ return M.current && (A = requestAnimationFrame(() => s())), () => {
116
+ var i;
117
+ cancelAnimationFrame(A), G(), v.current = ((i = R.current) == null ? void 0 : i.layout) === "masonry" ? null : { currentElement: d.currentElement, scrollOffset: d.scrollOffset }, a.current.clear(), p.current.clear(), c.current = null, d.detachScrollbar(e), d.dispose(), o.textContent = "", n.current = null, T(null);
99
118
  };
100
- }, [s]), B(() => {
101
- var l;
102
- const t = n.current;
103
- if (!t || W == null) return;
104
- const r = Q(_.current, ((l = g.current) == null ? void 0 : l.length) ?? null);
105
- if (r !== t.scroller.totalElements)
106
- if (t.scroller.updateTotalElements(r), t.scroller.currentElement > r - 1 && t.scroller.jumpToElement(r - 1), y.current) {
107
- const u = t.scroller;
119
+ }, [s, K, N]), J(() => {
120
+ var m, E;
121
+ const e = n.current;
122
+ if (!e || Q == null) return;
123
+ const t = le(W.current, ((m = y.current) == null ? void 0 : m.length) ?? null), l = ((E = R.current) == null ? void 0 : E.layout) === "masonry", o = l ? e.scroller.itemCount : e.scroller.totalElements;
124
+ if (t !== o) {
125
+ if (l) {
126
+ P();
127
+ return;
128
+ }
129
+ if (e.scroller.updateTotalElements(t), e.scroller.currentElement > t - 1 && e.scroller.jumpToElement(t - 1), M.current) {
130
+ const O = e.scroller;
108
131
  requestAnimationFrame(() => {
109
- s(), u.syncScrollbar();
132
+ s(), O.syncScrollbar();
110
133
  });
111
134
  } else
112
- t.scroller.syncScrollbar();
113
- }, [W, s]), B(() => {
114
- if (!n.current || !y.current) return;
115
- const t = requestAnimationFrame(() => s());
116
- return () => cancelAnimationFrame(t);
117
- }, [e.items, e.getItem]);
118
- const U = S(
119
- (t) => {
120
- const r = n.current;
121
- return r ? (r.scroller.jumpToElement(t), s()) : null;
135
+ e.scroller.syncScrollbar();
136
+ }
137
+ }, [Q, s]), J(() => {
138
+ if (!n.current || !M.current) return;
139
+ const e = requestAnimationFrame(() => s());
140
+ return () => cancelAnimationFrame(e);
141
+ }, [r.items, r.getItem]);
142
+ const se = b(
143
+ (e) => {
144
+ const t = n.current;
145
+ return t ? (t.scroller.jumpToElement(e), s()) : null;
146
+ },
147
+ [s]
148
+ ), ue = b(
149
+ (e, t = 0) => {
150
+ const l = n.current;
151
+ return l ? (l.scroller.jumpToItem(e, t), s()) : null;
122
152
  },
123
153
  [s]
124
- ), Z = S(
125
- (t) => {
126
- const r = n.current;
127
- return r ? (r.scroller.handleScrollPercentage(t), s()) : null;
154
+ ), ae = b(
155
+ (e) => {
156
+ const t = n.current;
157
+ return t ? (t.scroller.handleScrollPercentage(e), s()) : null;
128
158
  },
129
159
  [s]
130
- ), x = S(() => {
131
- const t = n.current;
132
- return t ? (t.scroller.reset(), s()) : null;
133
- }, [s]), ee = S(() => {
134
- const t = n.current;
135
- return t ? (t.scroller.clearAllCaches(), h.current.clear(), s()) : null;
136
- }, [s]), X = g.current, Y = !v.current && X != null ? X.length : null, k = b.current, p = h.current;
137
- c.current !== k && (p.clear(), c.current = k);
138
- const z = [];
139
- return f.current.forEach((t, r) => {
140
- if (Y !== null && r >= Y) {
141
- p.delete(r);
160
+ ), ie = b(() => {
161
+ const e = n.current;
162
+ return e ? (e.scroller.reset(), s()) : null;
163
+ }, [s]), me = b(() => {
164
+ const e = n.current;
165
+ return e ? (e.scroller.clearAllCaches(), p.current.clear(), s()) : null;
166
+ }, [s]), U = y.current, Z = !D.current && U != null ? U.length : null, X = S.current, C = p.current;
167
+ c.current !== X && (C.clear(), c.current = X);
168
+ const Y = [];
169
+ return a.current.forEach((e, t) => {
170
+ if (Z !== null && t >= Z) {
171
+ C.delete(t);
142
172
  return;
143
173
  }
144
- const l = D(r);
145
- let u = p.get(r);
146
- (!u || u.item !== l || u.mount !== t.mount) && (u = {
174
+ const l = z(t);
175
+ let o = C.get(t);
176
+ (!o || o.item !== l || o.mount !== e.mount) && (o = {
147
177
  item: l,
148
- mount: t.mount,
149
- node: J(k(l, r), t.mount, String(r))
150
- }, p.set(r, u)), z.push(u.node);
151
- }), p.size > f.current.size && p.forEach((t, r) => {
152
- f.current.has(r) || p.delete(r);
153
- }), e.tableHeader != null && w.current && z.push(J(e.tableHeader, w.current, "cerious-thead")), {
154
- containerRef: a,
155
- portals: z,
156
- scroller: F,
178
+ mount: e.mount,
179
+ node: ne(X(l, t), e.mount, String(t))
180
+ }, C.set(t, o)), Y.push(o.node);
181
+ }), C.size > a.current.size && C.forEach((e, t) => {
182
+ a.current.has(t) || C.delete(t);
183
+ }), r.tableHeader != null && I.current && Y.push(ne(r.tableHeader, I.current, "cerious-thead")), {
184
+ containerRef: f,
185
+ portals: Y,
186
+ scroller: q,
157
187
  render: s,
158
- jumpToElement: U,
159
- scrollToPercentage: Z,
160
- reset: x,
161
- recalculate: ee
188
+ jumpToElement: se,
189
+ jumpToItem: ue,
190
+ scrollToPercentage: ae,
191
+ reset: ie,
192
+ recalculate: me
162
193
  };
163
194
  }
164
- function ge(e, a) {
165
- const { className: n, style: f, "data-testid": h, children: c, ...E } = e, {
166
- containerRef: w,
167
- portals: M,
168
- scroller: H,
169
- render: F,
170
- jumpToElement: C,
171
- scrollToPercentage: b,
172
- reset: g,
173
- recalculate: v
174
- } = he(E);
175
- return se(
176
- a,
177
- () => ({ scroller: H, render: F, jumpToElement: C, scrollToPercentage: b, reset: g, recalculate: v }),
178
- [H, F, C, b, g, v]
179
- ), /* @__PURE__ */ te(
195
+ function Se(r, f) {
196
+ const { className: n, style: a, "data-testid": p, children: c, ...v } = r, {
197
+ containerRef: I,
198
+ portals: F,
199
+ scroller: L,
200
+ render: q,
201
+ jumpToElement: T,
202
+ jumpToItem: N,
203
+ scrollToPercentage: P,
204
+ reset: S,
205
+ recalculate: y
206
+ } = ve(v);
207
+ return ge(
208
+ f,
209
+ () => ({ scroller: L, render: q, jumpToElement: T, jumpToItem: N, scrollToPercentage: P, reset: S, recalculate: y }),
210
+ [L, q, T, N, P, S, y]
211
+ ), /* @__PURE__ */ fe(
180
212
  "div",
181
213
  {
182
- ref: w,
214
+ ref: I,
183
215
  className: n,
184
- "data-testid": h,
185
- style: { position: "relative", overflow: "hidden", ...f },
216
+ "data-testid": p,
217
+ style: { position: "relative", overflow: "hidden", ...a },
186
218
  children: [
187
219
  c,
188
- M
220
+ F
189
221
  ]
190
222
  }
191
223
  );
192
224
  }
193
- const Se = oe(ge);
225
+ const Me = he(Se);
194
226
  export {
195
- Se as CeriousScroll,
196
- ye as CeriousScrollEngine,
197
- he as useCeriousScroll
227
+ Me as CeriousScroll,
228
+ He as CeriousScrollEngine,
229
+ ve as useCeriousScroll
198
230
  };
@@ -1,6 +1,13 @@
1
1
  import { ReactNode, RefObject } from 'react';
2
- import { CeriousScroll as CeriousScrollEngine, CeriousScrollOptions, MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
2
+ import { CeriousScroll as CeriousScrollEngine, CeriousScrollOptions as CoreCeriousScrollOptions, MasonryOptions, MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
3
3
  import { CeriousViewportChangeDetail } from './viewport-change';
4
+ /** Engine options adapted for React-owned Masonry rendering. */
5
+ export type CeriousScrollOptions = Omit<CoreCeriousScrollOptions, 'masonry'> & {
6
+ masonry?: Omit<MasonryOptions, 'renderItem'> & {
7
+ /** Accepted for backward compatibility; React supplies the active renderer. */
8
+ renderItem?: MasonryOptions['renderItem'];
9
+ };
10
+ };
4
11
  export interface UseCeriousScrollOptions<TItem = unknown> {
5
12
  /** Total number of items. Falls back to `items.length` when omitted. */
6
13
  totalElements?: number | null;
@@ -38,6 +45,8 @@ export interface UseCeriousScrollResult {
38
45
  render: () => MeasuredViewportRange | null;
39
46
  /** Jump to an element index, then render. */
40
47
  jumpToElement: (index: number) => MeasuredViewportRange | null;
48
+ /** Masonry mode only: jump to a card index, then render. */
49
+ jumpToItem: (index: number, screenOffset?: number) => MeasuredViewportRange | null;
41
50
  /** Scroll to a percentage (0..100), then render. */
42
51
  scrollToPercentage: (percentage: number) => MeasuredViewportRange | null;
43
52
  /** Reset to the top, then render. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ceriousdevtech/react-cerious-scroll",
3
- "version": "1.0.7",
4
- "description": "React bindings for CeriousScroll — high-performance virtual scrolling with O(1) memory and no height estimation",
3
+ "version": "1.1.0",
4
+ "description": "React bindings for CeriousScroll — virtualized lists, native tables, and Masonry grids",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -44,6 +44,7 @@
44
44
  "large-lists",
45
45
  "data-grid",
46
46
  "variable-height",
47
+ "masonry",
47
48
  "o1-memory",
48
49
  "cerious-scroll"
49
50
  ],
@@ -56,7 +57,7 @@
56
57
  "demo:build": "vite build --config demo/vite.config.ts"
57
58
  },
58
59
  "dependencies": {
59
- "@ceriousdevtech/cerious-scroll": "^1.0.8"
60
+ "@ceriousdevtech/cerious-scroll": "^1.1.0"
60
61
  },
61
62
  "peerDependencies": {
62
63
  "react": ">=18 <20",