@ceriousdevtech/react-cerious-scroll 1.0.6 → 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,11 +5,32 @@ 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
+
20
+ ## [1.0.7] - 2026-06-24
21
+
22
+ ### Changed
23
+ - **A change in the item count now grows/shrinks the dataset in place instead of recreating the engine.** The hook calls `updateTotalElements()` (propagating the new count to navigation bounds, the scrollbar track, the renderer, and the height cache) rather than tearing down and rebuilding the scroller. A live append/prepend therefore keeps the scroll position and any in-progress scrollbar drag alive, and an appended dataset keeps a stable bottom index instead of a bouncing tail. A shrink that leaves the position past the new end is clamped, and the scrollbar thumb is re-synced after the lengthened track re-renders. No public API change.
24
+ - Updated the core engine dependency to `@ceriousdevtech/cerious-scroll@^1.0.8` (native-scrollbar drag rendering is now coalesced to one render per frame, with no per-row layout thrash on fast drags).
25
+
8
26
  ## [1.0.6] - 2026-06-11
9
27
 
10
28
  ### Fixed
11
29
  - **`recalculate()` now refreshes row content, not just heights.** It re-runs `renderItem` for every currently-rendered row (busting the per-index portal cache), so rows whose content depends on state outside `(item, index)` — e.g. a sliding/windowed data source where index → content shifts — update on an in-place refresh. Previously `recalculate()` only cleared the engine's height caches and re-measured, leaving such rows showing stale content; this now matches the Vue/Angular wrappers, whose `recalculate()` re-runs the row template/slot. A plain scroll still reuses cached portals for unchanged rows, so heavy rows stay cheap to scroll.
12
30
 
31
+ ### Changed
32
+ - Updated the core engine dependency to `@ceriousdevtech/cerious-scroll@^1.0.7` (scrollbar drag-to-top fix).
33
+
13
34
  ## [1.0.5] - 2026-06-08
14
35
 
15
36
  ### Added
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 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 u=e.querySelector(`[${$}]`);if(u)return u;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,u){const n=h=>{const l=h.detail;l&&u({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&&u({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,u){const n=typeof e=="number"?e:typeof u=="number"?u: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 u=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},[]),s=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=u.current;if(!t)return;const c=ne(t),o=L.current??{},a=o.onScroll,k={...o,onScroll:()=>{a==null||a(),q.current&&s()}};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(()=>s())),()=>{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,s]),r.useEffect(()=>{if(!n.current||!q.current)return;const t=requestAnimationFrame(()=>s());return()=>cancelAnimationFrame(t)},[e.items,e.getItem]);const Q=r.useCallback(t=>{const c=n.current;return c?(c.scroller.jumpToElement(t),s()):null},[s]),U=r.useCallback(t=>{const c=n.current;return c?(c.scroller.handleScrollPercentage(t),s()):null},[s]),Z=r.useCallback(()=>{const t=n.current;return t?(t.scroller.reset(),s()):null},[s]),x=r.useCallback(()=>{const t=n.current;return t?(t.scroller.clearAllCaches(),h.current.clear(),s()):null},[s]),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:u,portals:V,scroller:A,render:s,jumpToElement:Q,scrollToPercentage:U,reset:Z,recalculate:x}}function oe(e,u){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(u,()=>({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 ue=r.forwardRef(oe);Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>G.CeriousScroll});exports.CeriousScroll=ue;exports.useCeriousScroll=J;
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,185 +1,230 @@
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
- const r = document.createElement("div");
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;
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
+ const n = document.createElement("div");
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 me(e, s) {
15
- const r = (h) => {
16
- const c = h.detail;
17
- c && s({
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 && s({
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", r), e.addEventListener("viewport-change", f), () => {
33
- e.removeEventListener("cerious-viewport-change", r), 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 de(e, s) {
37
- const r = typeof e == "number" ? e : typeof s == "number" ? s : void 0;
38
- if (r === void 0 || Number.isNaN(r))
36
+ function le(r, f) {
37
+ const n = typeof r == "number" ? r : typeof f == "number" ? f : void 0;
38
+ if (n === void 0 || Number.isNaN(n))
39
39
  throw new Error("useCeriousScroll: provide `totalElements` or `items`.");
40
- return Math.max(1, Math.floor(r));
40
+ return Math.max(1, Math.floor(n));
41
41
  }
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
- if (n) return n(t);
49
- const l = E.current;
50
- return l ? l[t] : void 0;
51
- }, []), u = w(() => {
52
- var T, O;
53
- const t = r.current;
54
- if (!t) return null;
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
- if (!t) return;
71
- const n = fe(t), l = _.current ?? {}, a = l.onScroll, j = {
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
- a == null || a(), F.current && u();
87
+ m == null || m(), M.current && s();
75
88
  }
76
89
  };
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);
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 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);
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
- (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);
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
- }, [K, u]), Y(() => {
101
- if (!r.current || !F.current) return;
102
- const t = requestAnimationFrame(() => u());
103
- return () => cancelAnimationFrame(t);
104
- }, [e.items, e.getItem]);
105
- const Q = w(
106
- (t) => {
107
- const n = r.current;
108
- return n ? (n.scroller.jumpToElement(t), u()) : null;
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;
131
+ requestAnimationFrame(() => {
132
+ s(), O.syncScrollbar();
133
+ });
134
+ } else
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;
109
152
  },
110
- [u]
111
- ), U = w(
112
- (t) => {
113
- const n = r.current;
114
- return n ? (n.scroller.handleScrollPercentage(t), u()) : null;
153
+ [s]
154
+ ), ae = b(
155
+ (e) => {
156
+ const t = n.current;
157
+ return t ? (t.scroller.handleScrollPercentage(e), s()) : null;
115
158
  },
116
- [u]
117
- ), Z = w(() => {
118
- const t = r.current;
119
- return t ? (t.scroller.reset(), u()) : null;
120
- }, [u]), x = w(() => {
121
- const t = r.current;
122
- return t ? (t.scroller.clearAllCaches(), h.current.clear(), 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);
159
+ [s]
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);
129
172
  return;
130
173
  }
131
- const l = B(n);
132
- let a = p.get(n);
133
- (!a || a.item !== l || a.mount !== t.mount) && (a = {
174
+ const l = z(t);
175
+ let o = C.get(t);
176
+ (!o || o.item !== l || o.mount !== e.mount) && (o = {
134
177
  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
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,
187
+ render: s,
188
+ jumpToElement: se,
189
+ jumpToItem: ue,
190
+ scrollToPercentage: ae,
191
+ reset: ie,
192
+ recalculate: me
149
193
  };
150
194
  }
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(
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(
167
212
  "div",
168
213
  {
169
- ref: C,
170
- className: r,
171
- "data-testid": h,
172
- style: { position: "relative", overflow: "hidden", ...f },
214
+ ref: I,
215
+ className: n,
216
+ "data-testid": p,
217
+ style: { position: "relative", overflow: "hidden", ...a },
173
218
  children: [
174
219
  c,
175
- M
220
+ F
176
221
  ]
177
222
  }
178
223
  );
179
224
  }
180
- const we = oe(ge);
225
+ const Me = he(Se);
181
226
  export {
182
- we as CeriousScroll,
183
- ye as CeriousScrollEngine,
184
- he as useCeriousScroll
227
+ Me as CeriousScroll,
228
+ He as CeriousScrollEngine,
229
+ ve as useCeriousScroll
185
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.6",
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.7"
60
+ "@ceriousdevtech/cerious-scroll": "^1.1.0"
60
61
  },
61
62
  "peerDependencies": {
62
63
  "react": ">=18 <20",