core
Source
// Basic List — React implementation using vlist-react adapter
// Interactive control panel demonstrating core vlist API: item count, overscan,
// scrollToIndex, and data operations (append, prepend, remove).
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { useVList, useVListEvent } from "vlist-react";
import {
DEFAULT_COUNT,
ITEM_HEIGHT,
makeUser,
makeUsers,
itemTemplate,
} from "../shared.js";
// =============================================================================
// VListPanel — owns the vlist instance, reports events + instance to parent
// =============================================================================
function VListPanel({
users,
overscan,
onReady,
onSelectionChange,
onRangeChange,
}: {
users: any[];
overscan: number;
onReady: (instance: any) => void;
onSelectionChange: (selected: number[]) => void;
onRangeChange: (range: { start: number; end: number }) => void;
}) {
const { containerRef, instanceRef } = useVList({
ariaLabel: "User list",
overscan,
items: users,
item: {
height: ITEM_HEIGHT,
template: itemTemplate,
},
selection: { mode: "single" },
});
useVListEvent(instanceRef, "selection:change", ({ selected }: any) => {
onSelectionChange(selected);
});
useVListEvent(instanceRef, "range:change", ({ range }: any) => {
onRangeChange(range);
});
// Expose instance to parent after mount
useEffect(() => {
if (instanceRef.current) onReady(instanceRef.current);
}, [instanceRef.current]);
return <div ref={containerRef} id="list-container" />;
}
// =============================================================================
// App Component
// =============================================================================
function App() {
// State
const [users, setUsers] = useState(() => makeUsers(DEFAULT_COUNT));
const [nextId, setNextId] = useState(DEFAULT_COUNT + 1);
const [overscan, setOverscan] = useState(3);
const [scrollIndex, setScrollIndex] = useState(0);
const [scrollAlign, setScrollAlign] = useState<"start" | "center" | "end">(
"start",
);
const [domCount, setDomCount] = useState(0);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [instance, setInstance] = useState<any>(null);
// Derived values
const total = users.length;
const visiblePercent = total > 0 ? Math.round((domCount / total) * 100) : 0;
const memorySaved = total > 0 ? Math.round((1 - domCount / total) * 100) : 0;
// Navigation
const handleGoToIndex = () => {
const clamped = Math.max(0, Math.min(scrollIndex, total - 1));
instance?.scrollToIndex(clamped, {
align: scrollAlign,
behavior: "smooth",
duration: 400,
});
};
const scrollToFirst = () => {
instance?.scrollToIndex(0, { behavior: "smooth", duration: 300 });
};
const scrollToMiddle = () => {
instance?.scrollToIndex(Math.floor(total / 2), {
align: "center",
behavior: "smooth",
duration: 500,
});
};
const scrollToLast = () => {
instance?.scrollToIndex(total - 1, {
align: "end",
behavior: "smooth",
duration: 500,
});
};
// Count slider
const handleCountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const count = parseInt(e.target.value, 10);
setUsers(makeUsers(count));
setNextId(count + 1);
};
// Overscan slider
const handleOverscanChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOverscan(parseInt(e.target.value, 10));
};
// Data operations
const handleAppend = () => {
const newUser = makeUser(nextId);
setNextId((n) => n + 1);
setUsers((prev) => [...prev, newUser]);
instance?.appendItems([newUser]);
};
const handlePrepend = () => {
const newUser = makeUser(nextId);
setNextId((n) => n + 1);
setUsers((prev) => [newUser, ...prev]);
instance?.prependItems([newUser]);
};
const handleAppend100 = () => {
const batch = makeUsers(100, nextId);
setNextId((n) => n + 100);
setUsers((prev) => [...prev, ...batch]);
instance?.appendItems(batch);
};
const handleRemove = () => {
if (users.length === 0) return;
const idx =
selectedIndex >= 0 && selectedIndex < users.length
? selectedIndex
: users.length - 1;
instance?.clearSelection();
setSelectedIndex(-1);
const newUsers = users.filter((_, i) => i !== idx);
setUsers(newUsers);
instance?.setItems(newUsers);
};
const handleClear = () => {
setUsers([]);
instance?.setItems([]);
};
const handleReset = () => {
setUsers(makeUsers(DEFAULT_COUNT));
setNextId(DEFAULT_COUNT + 1);
setOverscan(3);
setSelectedIndex(-1);
};
return (
<div className="container">
<header>
<h1>Basic List</h1>
<p className="description">
React implementation — the core of <code>@floor/vlist</code>. Use the
control panel to explore item count, overscan, scroll-to, and data
operations in real time.
</p>
</header>
<div className="split-layout">
<div className="split-main">
{/* key={overscan} remounts VListPanel so useVList picks up new overscan */}
<VListPanel
key={overscan}
users={users}
overscan={overscan}
onReady={setInstance}
onSelectionChange={(selected) => {
setSelectedIndex(selected.length > 0 ? selected[0] : -1);
}}
onRangeChange={(range) => {
setDomCount(range.end - range.start + 1);
}}
/>
</div>
<aside className="split-panel">
{/* Items */}
<section className="panel-section">
<h3 className="panel-title">Items</h3>
<div className="panel-row">
<label className="panel-label">Count</label>
<span className="panel-value">{total.toLocaleString()}</span>
</div>
<div className="panel-row">
<input
type="range"
className="panel-slider"
min="100"
max="100000"
step="100"
value={Math.min(total, 100000)}
onChange={handleCountChange}
/>
</div>
<div className="panel-row">
<label className="panel-label">Overscan</label>
<span className="panel-value">{overscan}</span>
</div>
<div className="panel-row">
<input
type="range"
className="panel-slider"
min="0"
max="10"
step="1"
value={overscan}
onChange={handleOverscanChange}
/>
</div>
</section>
{/* Scroll To */}
<section className="panel-section">
<h3 className="panel-title">Scroll To</h3>
<div className="panel-row">
<div className="panel-input-group">
<input
type="number"
className="panel-input"
placeholder="Index"
min="0"
value={scrollIndex}
onChange={(e) =>
setScrollIndex(parseInt(e.target.value, 10) || 0)
}
onKeyDown={(e) => {
if (e.key === "Enter") handleGoToIndex();
}}
/>
<select
className="panel-select"
value={scrollAlign}
onChange={(e) =>
setScrollAlign(e.target.value as typeof scrollAlign)
}
>
<option value="start">start</option>
<option value="center">center</option>
<option value="end">end</option>
</select>
<button className="panel-btn" onClick={handleGoToIndex}>
Go
</button>
</div>
</div>
<div className="panel-row">
<div className="panel-btn-group">
<button
className="panel-btn panel-btn--icon"
title="First"
onClick={scrollToFirst}
>
<i className="icon icon--up"></i>
</button>
<button
className="panel-btn panel-btn--icon"
title="Middle"
onClick={scrollToMiddle}
>
<i className="icon icon--center"></i>
</button>
<button
className="panel-btn panel-btn--icon"
title="Last"
onClick={scrollToLast}
>
<i className="icon icon--down"></i>
</button>
</div>
</div>
</section>
{/* Data Operations */}
<section className="panel-section">
<h3 className="panel-title">Data</h3>
<div className="panel-row">
<div className="panel-btn-group">
<button
className="panel-btn"
title="Prepend 1 item"
onClick={handlePrepend}
>
<i className="icon icon--add"></i> Prepend
</button>
<button
className="panel-btn"
title="Append 1 item"
onClick={handleAppend}
>
<i className="icon icon--add"></i> Append
</button>
<button
className="panel-btn"
title="Append 100 items"
onClick={handleAppend100}
>
<i className="icon icon--add"></i> +100
</button>
</div>
</div>
<div className="panel-row">
<div className="panel-btn-group">
<button
className="panel-btn"
title="Remove selected or last item"
onClick={handleRemove}
>
<i className="icon icon--remove"></i> Remove
</button>
<button
className="panel-btn"
title="Clear all items"
onClick={handleClear}
>
<i className="icon icon--trash"></i> Clear
</button>
<button
className="panel-btn"
title="Reset to 10,000 items"
onClick={handleReset}
>
<i className="icon icon--shuffle"></i> Reset
</button>
</div>
</div>
</section>
</aside>
</div>
<footer className="example-footer" id="example-footer">
<div className="example-footer__left">
<span className="example-footer__stat">
<strong>{visiblePercent}%</strong>
</span>
<span className="example-footer__stat">
{domCount} / <strong>{total.toLocaleString()}</strong>
<span className="example-footer__unit"> items</span>
</span>
<span className="example-footer__stat">
<strong>{memorySaved}%</strong>
<span className="example-footer__unit"> saved</span>
</span>
</div>
<div className="example-footer__right">
<span className="example-footer__stat">
height <strong>{ITEM_HEIGHT}</strong>
<span className="example-footer__unit">px</span>
</span>
<span className="example-footer__stat">
overscan <strong>{overscan}</strong>
</span>
</div>
</footer>
</div>
);
}
// =============================================================================
// Mount
// =============================================================================
createRoot(document.getElementById("react-root")!).render(<App />);
<div id="react-root"></div>
// Shared data and utilities for basic list example variants
// This file is imported by all framework implementations to avoid duplication
import { makeUser, makeUsers } from '../../src/data/people.js';
// =============================================================================
// Constants
// =============================================================================
export const DEFAULT_COUNT = 10_000;
export const ITEM_HEIGHT = 56;
// =============================================================================
// Data Generation (re-export for convenience)
// =============================================================================
export { makeUser, makeUsers };
// =============================================================================
// Templates
// =============================================================================
export const itemTemplate = (user, i) => `
<div class="item__avatar" style="background:${user.color}">${user.initials}</div>
<div class="item__text">
<div class="item__name">${user.name}</div>
<div class="item__email">${user.email}</div>
</div>
<span class="item__index">#${i + 1}</span>
`;
/* Basic List — example styles */
/* ============================================================================
Item (styles live on .vlist-item — no wrapper div needed)
============================================================================ */
.vlist-item {
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
}
.item__avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 15px;
flex-shrink: 0;
}
.item__text {
flex: 1;
min-width: 0;
}
.item__name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item__email {
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item__index {
font-size: 12px;
color: var(--text-muted);
min-width: 48px;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ============================================================================
Selected state
============================================================================ */