Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to CeriousScroll will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.3] - 2026-06-03

### Added
- **Horizontal momentum** in `TouchController`. When `touch.getHorizontalScrollTarget` is provided, horizontal flicks now decay with the same iOS-style cubic-bezier easing already used for vertical scrolling, including boundary-hit early-exit so momentum stops cleanly at the left/right edges instead of burning frames.
- **Custom scrollbar thumb** in `NativeScrollbar`. The sibling-driver strip now renders a styled thumb that floats over the host container's right edge. Touch hit zone covers the full strip so a tap anywhere in the column starts a drag. Appearance is themable via CSS variables on the scroll host: `--cerious-thumb-color`, `--cerious-thumb-color-active`, `--cerious-thumb-width`, `--cerious-thumb-width-active`. One stylesheet is injected lazily on first use; SSR/non-DOM environments are unaffected.

### Changed
- Renamed the internal `velocityY` velocity accumulator to `velocity` and made it axis-aware so the same momentum pipeline can drive vertical engine scroll or horizontal `scrollLeft` writes.

## [1.0.2] - 2026-06-01

### Changed
Expand Down
36 changes: 32 additions & 4 deletions demo/data-grid-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,23 @@
border: 1px solid #e0e0e0;
}

/* Horizontal scroll wrapper: header + virtual rows live inside this
* element so the native horizontal scrollbar shifts both together.
* The cerious-scroll sibling scrollbar strip stays a direct child of
* #gridViewport and remains pinned to the visible right edge. */
.grid-h-scroll {
position: absolute;
inset: 0;
overflow-x: auto;
overflow-y: hidden;
}

#scrollContent {
position: absolute;
top: 0;
left: 0;
width: 100%;
min-width: 1210px;
width: max-content;
pointer-events: none;
overflow: visible;
}
Expand All @@ -138,6 +150,7 @@
border-bottom: 2px solid #dee2e6;
display: grid;
grid-template-columns: 50px 120px 150px 220px 120px 100px 140px 120px 80px 110px;
min-width: 1210px;
font-weight: 600;
font-size: 0.85rem;
color: #495057;
Expand Down Expand Up @@ -173,6 +186,7 @@
.grid-row {
display: grid;
grid-template-columns: 50px 120px 150px 220px 120px 100px 140px 120px 80px 110px;
min-width: 1210px;
border-bottom: 1px solid #e9ecef;
transition: background 0.1s;
pointer-events: auto;
Expand Down Expand Up @@ -502,7 +516,14 @@
// Setup viewport
const viewport = document.getElementById('gridViewport');
viewport.innerHTML = '';


// Horizontal scroll wrapper holds header + virtual rows so they shift
// together; cerious-scroll's sibling scrollbar stays a direct child
// of `viewport` and remains pinned to the visible right edge.
const hScroll = document.createElement('div');
hScroll.className = 'grid-h-scroll';
viewport.appendChild(hScroll);

// Add sticky header
const headerRow = document.createElement('div');
headerRow.className = 'grid-header-row';
Expand All @@ -518,12 +539,12 @@
<div class="grid-header-cell" data-column="score">Score <span class="sort-icon">⇅</span></div>
<div class="grid-header-cell" data-column="date">Date <span class="sort-icon">⇅</span></div>
`;
viewport.appendChild(headerRow);
hScroll.appendChild(headerRow);

const scrollContent = document.createElement('div');
scrollContent.id = 'scrollContent';
scrollContent.style.marginTop = '48px'; // Account for sticky header
viewport.appendChild(scrollContent);
hScroll.appendChild(scrollContent);

// Column sorting
headerRow.querySelectorAll('.grid-header-cell[data-column]').forEach(cell => {
Expand Down Expand Up @@ -558,6 +579,13 @@
viewport,
filteredIndices.length,
{
touch: {
enabled: true,
// Forward horizontal swipes to the wrapper that owns the
// native overflow-x scrollbar so the column header and
// rows scroll together on mobile.
getHorizontalScrollTarget: () => hScroll
},
onScroll: () => {
scroller.renderViewport(
viewport.clientHeight - 48, // Account for header
Expand Down
24 changes: 22 additions & 2 deletions demo/shared-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ body {
padding: 12px;
border-radius: 6px;
border: 1px solid #e0e0e0;
min-width: 0;
}

.control-section h3 {
Expand Down Expand Up @@ -170,7 +171,7 @@ body {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: nowrap;
flex-wrap: wrap;
}

.control-group select:focus,
Expand Down Expand Up @@ -380,6 +381,8 @@ body {
color: #495057;
font-size: 0.95rem;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; /* Show ellipsis for very long text */
}
Expand Down Expand Up @@ -598,7 +601,24 @@ body {
}

.header h1 {
font-size: 2rem;
font-size: 1.35rem;
line-height: 1.2;
}

.subtitle {
font-size: 0.85rem;
}

body {
padding: 10px;
}

.app-container {
padding: 12px;
}

.input-button-group {
flex-wrap: wrap;
}

.metric-value {
Expand Down
59 changes: 51 additions & 8 deletions demo/sql-query-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,19 @@
flex: 1;
display: flex;
flex-direction: column;
overflow-x: auto;
overflow-y: hidden;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.table-header {
display: flex;
background: #2d2d30;
border-bottom: 2px solid #0e639c;
flex-shrink: 0;
min-width: fit-content;
position: sticky;
top: 0;
z-index: 10;
}
.column-header {
padding: 8px 12px;
Expand Down Expand Up @@ -196,18 +199,29 @@
flex: 1;
position: relative;
overflow: hidden;
min-width: fit-content;
min-width: 0;
}
/* Horizontal scroll wrapper inside the cerious-scroll container so
* the sibling vertical scrollbar strip stays pinned to the visible
* right edge while header + virtual rows shift together. */
.results-hscroll {
position: absolute;
inset: 0;
overflow-x: auto;
overflow-y: hidden;
}
.scroll-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
width: max-content;
min-width: 100%;
}
.result-row {
display: flex;
border-bottom: 1px solid #2d2d30;
min-height: 32px;
min-width: fit-content;
}
.result-row:hover {
background: rgba(255, 255, 255, 0.03);
Expand Down Expand Up @@ -641,6 +655,11 @@ <h1>SQL Query Results Viewer</h1>
const viewport = document.getElementById('resultsViewport');
const header = document.getElementById('tableHeader');

// Header may already be parented to a previous .results-hscroll
// wrapper from an earlier render — detach so the upcoming
// viewport.innerHTML clear doesn't destroy it.
if (header.parentElement) header.parentElement.removeChild(header);

// Clear empty state
viewport.innerHTML = '';

Expand Down Expand Up @@ -680,17 +699,41 @@ <h1>SQL Query Results Viewer</h1>
this.scroller = null;
}

// Create horizontal scroll wrapper and move the header into it.
// Putting both the header and the virtual scroll content inside
// this wrapper means a single native overflow-x scroller shifts
// them together, while .results-viewport (the cerious-scroll
// container) keeps its visible width so the vertical scrollbar
// strip stays pinned to the right edge.
const hScroll = document.createElement('div');
hScroll.className = 'results-hscroll';
viewport.appendChild(hScroll);
hScroll.appendChild(header);

// Create scroll content container
const scrollContent = document.createElement('div');
scrollContent.className = 'scroll-content';
scrollContent.id = 'scrollContent';
viewport.appendChild(scrollContent);

hScroll.appendChild(scrollContent);

// Offset the absolutely-positioned virtual rows by the sticky
// header height so the first row isn't hidden underneath it.
const headerHeight = header.offsetHeight;
scrollContent.style.top = `${headerHeight}px`;
const getRowAreaHeight = () => Math.max(0, viewport.clientHeight - headerHeight);

// Initialize CeriousScroll
this.scroller = new CeriousScrollClass(viewport, this.filteredData.length, {
touch: {
enabled: true,
// Forward horizontal swipes to the inner wrapper that
// owns the native overflow-x scrollbar so the column
// header and rows scroll together on mobile.
getHorizontalScrollTarget: () => hScroll
},
onScroll: () => {
this.scroller.renderViewport(
viewport.clientHeight,
getRowAreaHeight(),
scrollContent,
(index, element) => this.renderRow(index, element)
);
Expand All @@ -700,7 +743,7 @@ <h1>SQL Query Results Viewer</h1>
// Initial render
this.scroller.jumpToElement(0);
scrollContent.innerHTML = '';
this.scroller.renderViewport(viewport.clientHeight, scrollContent,
this.scroller.renderViewport(getRowAreaHeight(), scrollContent,
(index, element) => this.renderRow(index, element));

// Start performance monitoring
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ceriousdevtech/cerious-scroll",
"version": "1.0.2",
"version": "1.0.3",
"description": "High-performance virtual scrolling library with O(1) memory usage, supporting millions of elements",
"type": "module",
"main": "dist/index.js",
Expand Down
Loading