/ Examples
core
Source
// Basic List — SolidJS implementation using vlist-solidjs adapter
// Interactive control panel demonstrating core vlist API: item count, overscan,
// scrollToIndex, and data operations (append, prepend, remove).

import { render } from "solid-js/web";
import { createSignal, createMemo } from "solid-js";
import { createVList, createVListEvent } from "vlist-solidjs";
import {
  DEFAULT_COUNT,
  ITEM_HEIGHT,
  makeUser,
  makeUsers,
  itemTemplate,
} from "../shared.js";

// =============================================================================
// App Component
// =============================================================================

function App() {
  // State
  const [users, setUsers] = createSignal(makeUsers(DEFAULT_COUNT));
  const [nextId, setNextId] = createSignal(DEFAULT_COUNT + 1);
  const [overscan, setOverscan] = createSignal(3);
  const [scrollIndex, setScrollIndex] = createSignal(0);
  const [scrollAlign, setScrollAlign] = createSignal("start");
  const [domCount, setDomCount] = createSignal(0);
  const [selectedIndex, setSelectedIndex] = createSignal(-1);

  // Config accessor — createVList reacts to items changes automatically
  const config = () => ({
    ariaLabel: "User list",
    overscan: overscan(),
    items: users(),
    item: {
      height: ITEM_HEIGHT,
      template: itemTemplate,
    },
    selection: { mode: "single" },
  });

  // Create vlist via the SolidJS adapter
  const { setRef, instance } = createVList(config);

  // Bind events via the adapter helper
  createVListEvent(instance, "selection:change", ({ selected }) => {
    setSelectedIndex(selected.length > 0 ? selected[0] : -1);
  });

  createVListEvent(instance, "range:change", ({ range }) => {
    setDomCount(range.end - range.start + 1);
  });

  // Derived values
  const total = () => users().length;

  const visiblePercent = () => {
    return total() > 0 ? Math.round((domCount() / total()) * 100) : 0;
  };

  const memorySaved = () => {
    return 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) => {
    const count = parseInt(e.target.value, 10);
    setUsers(makeUsers(count));
    setNextId(count + 1);
  };

  // Overscan slider
  const handleOverscanChange = (e) => {
    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 = () => {
    const current = users();
    if (current.length === 0) return;
    const idx =
      selectedIndex() >= 0 && selectedIndex() < current.length
        ? selectedIndex()
        : current.length - 1;
    instance()?.clearSelection();
    setSelectedIndex(-1);
    setUsers((prev) => prev.filter((_, i) => i !== idx));
    instance()?.setItems(users());
  };

  const handleClear = () => {
    setUsers([]);
    instance()?.setItems([]);
  };

  const handleReset = () => {
    const newUsers = makeUsers(DEFAULT_COUNT);
    setUsers(newUsers);
    setNextId(DEFAULT_COUNT + 1);
    setOverscan(3);
  };

  return (
    <div class="container">
      <header>
        <h1>Basic List</h1>
        <p class="description">
          SolidJS 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 class="split-layout">
        <div class="split-main">
          <div ref={setRef} id="list-container" />
        </div>

        <aside class="split-panel">
          {/* Items */}
          <section class="panel-section">
            <h3 class="panel-title">Items</h3>

            <div class="panel-row">
              <label class="panel-label">Count</label>
              <span class="panel-value">{total().toLocaleString()}</span>
            </div>
            <div class="panel-row">
              <input
                type="range"
                class="panel-slider"
                min="100"
                max="100000"
                step="100"
                value={Math.min(total(), 100000)}
                onChange={handleCountChange}
              />
            </div>

            <div class="panel-row">
              <label class="panel-label">Overscan</label>
              <span class="panel-value">{overscan()}</span>
            </div>
            <div class="panel-row">
              <input
                type="range"
                class="panel-slider"
                min="0"
                max="10"
                step="1"
                value={overscan()}
                onChange={handleOverscanChange}
              />
            </div>
          </section>

          {/* Scroll To */}
          <section class="panel-section">
            <h3 class="panel-title">Scroll To</h3>

            <div class="panel-row">
              <div class="panel-input-group">
                <input
                  type="number"
                  class="panel-input"
                  placeholder="Index"
                  min="0"
                  value={scrollIndex()}
                  onInput={(e) =>
                    setScrollIndex(parseInt(e.target.value, 10) || 0)
                  }
                  onKeyDown={(e) => {
                    if (e.key === "Enter") handleGoToIndex();
                  }}
                />
                <select
                  class="panel-select"
                  value={scrollAlign()}
                  onChange={(e) => setScrollAlign(e.target.value)}
                >
                  <option value="start">start</option>
                  <option value="center">center</option>
                  <option value="end">end</option>
                </select>
                <button class="panel-btn" onClick={handleGoToIndex}>
                  Go
                </button>
              </div>
            </div>

            <div class="panel-row">
              <div class="panel-btn-group">
                <button
                  class="panel-btn panel-btn--icon"
                  title="First"
                  onClick={scrollToFirst}
                >
                  <i class="icon icon--up" />
                </button>
                <button
                  class="panel-btn panel-btn--icon"
                  title="Middle"
                  onClick={scrollToMiddle}
                >
                  <i class="icon icon--center" />
                </button>
                <button
                  class="panel-btn panel-btn--icon"
                  title="Last"
                  onClick={scrollToLast}
                >
                  <i class="icon icon--down" />
                </button>
              </div>
            </div>
          </section>

          {/* Data Operations */}
          <section class="panel-section">
            <h3 class="panel-title">Data</h3>

            <div class="panel-row">
              <div class="panel-btn-group">
                <button
                  class="panel-btn"
                  title="Prepend 1 item"
                  onClick={handlePrepend}
                >
                  <i class="icon icon--add" /> Prepend
                </button>
                <button
                  class="panel-btn"
                  title="Append 1 item"
                  onClick={handleAppend}
                >
                  <i class="icon icon--add" /> Append
                </button>
                <button
                  class="panel-btn"
                  title="Append 100 items"
                  onClick={handleAppend100}
                >
                  <i class="icon icon--add" /> +100
                </button>
              </div>
            </div>

            <div class="panel-row">
              <div class="panel-btn-group">
                <button
                  class="panel-btn"
                  title="Remove selected or last item"
                  onClick={handleRemove}
                >
                  <i class="icon icon--remove" /> Remove
                </button>
                <button
                  class="panel-btn"
                  title="Clear all items"
                  onClick={handleClear}
                >
                  <i class="icon icon--trash" /> Clear
                </button>
                <button
                  class="panel-btn"
                  title="Reset to 10,000 items"
                  onClick={handleReset}
                >
                  <i class="icon icon--shuffle" /> Reset
                </button>
              </div>
            </div>
          </section>
        </aside>
      </div>

      <footer class="example-footer" id="example-footer">
        <div class="example-footer__left">
          <span class="example-footer__stat">
            <strong>{visiblePercent()}%</strong>
          </span>
          <span class="example-footer__stat">
            {domCount()} / <strong>{total().toLocaleString()}</strong>
            <span class="example-footer__unit"> items</span>
          </span>
          <span class="example-footer__stat">
            <strong>{memorySaved()}%</strong>
            <span class="example-footer__unit"> saved</span>
          </span>
        </div>
        <div class="example-footer__right">
          <span class="example-footer__stat">
            height <strong>{ITEM_HEIGHT}</strong>
            <span class="example-footer__unit">px</span>
          </span>
          <span class="example-footer__stat">
            overscan <strong>{overscan()}</strong>
          </span>
        </div>
      </footer>
    </div>
  );
}

// =============================================================================
// Mount
// =============================================================================

render(() => <App />, document.getElementById("solidjs-root"));
<div id="solidjs-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
   ============================================================================ */