/ Examples
asyncselectiontablegrid

Track List

SQLite-backed music database with CRUD operations

Tracks

0% 0.00 / 0.00 px/ms 0 / 0 items
list
Source
// Track List - Pure Vanilla JavaScript with Async Loading
// Demonstrates vlist with lazy-loaded SQLite data, fetching tracks in chunks of 25
// Layout mode toggle: List ↔ Grid ↔ Table

import {
  vlist,
  withSelection,
  withAsync,
  withGrid,
  withTable,
  withScrollbar,
  withScale,
} from "vlist";
import { createStats } from "../stats.js";
import { createInfoUpdater } from "../info.js";
import {
  API_BASE,
  trackTemplate,
  trackGridTemplate,
  trackTableColumns,
  trackTableRowTemplate,
  formatSelectionCount,
  formatDuration,
  escapeHtml,
} from "./shared.js";

// =============================================================================
// Constants
// =============================================================================

const CHUNK_SIZE = 25;
const ITEM_HEIGHT = 56;
const GRID_COLUMNS = 4;
const GRID_GAP = 8;
const TABLE_ROW_HEIGHT = 36;
const TABLE_HEADER_HEIGHT = 36;

// =============================================================================
// State
// =============================================================================

let list = null;
let totalTracks = 0;
let currentSelectionMode = "single";
let currentLayoutMode = "list";
let currentScrollbarEnabled = false;
let currentScaleEnabled = false;
let loadRequests = 0;
let loadedCount = 0;

let currentFilters = {
  search: "",
  country: "",
  decade: "",
};

// =============================================================================
// Adapter - fetches tracks from SQLite API in chunks
// =============================================================================

function buildParams(offset, limit) {
  const params = new URLSearchParams({
    offset: String(offset),
    limit: String(limit),
    sort: "id",
    direction: "desc",
  });

  if (currentFilters.search) params.set("search", currentFilters.search);
  if (currentFilters.country) params.set("country", currentFilters.country);
  if (currentFilters.decade) params.set("decade", currentFilters.decade);

  return params;
}

const tracksAdapter = {
  read: async ({ offset, limit }) => {
    loadRequests++;

    const params = buildParams(offset, limit);
    const res = await fetch(`${API_BASE}?${params}`);
    if (!res.ok) throw new Error(`API error: ${res.status}`);
    const data = await res.json();

    totalTracks = data.total;
    loadedCount += data.items.length;

    return {
      items: data.items,
      total: data.total,
      hasMore: data.hasMore,
    };
  },
};

// =============================================================================
// Stats — info bar (progress, velocity, visible/total)
// =============================================================================

function getEffectiveItemHeight() {
  if (currentLayoutMode === "table") return TABLE_ROW_HEIGHT;
  if (currentLayoutMode === "grid") {
    const container = document.getElementById("list-container");
    if (!container) return 200;
    const innerWidth = container.clientWidth - 2;
    const colWidth =
      (innerWidth - (GRID_COLUMNS - 1) * GRID_GAP) / GRID_COLUMNS;
    return Math.round(colWidth * 1.3);
  }
  return ITEM_HEIGHT;
}

const stats = createStats({
  getScrollPosition: () => list?.getScrollPosition() ?? 0,
  getTotal: () => totalTracks,
  getItemSize: () => getEffectiveItemHeight(),
  getColumns: () => (currentLayoutMode === "grid" ? GRID_COLUMNS : 1),
  getContainerSize: () => {
    const el = document.getElementById("list-container");
    return el ? el.clientHeight : 0;
  },
});

const updateInfo = createInfoUpdater(stats);

// =============================================================================
// DOM References
// =============================================================================

// Layout
const layoutModeEl = document.getElementById("layout-mode");
const scrollbarToggle = document.getElementById("scrollbar-toggle");
const scaleToggle = document.getElementById("scale-toggle");

// Selection
const selectionModeEl = document.getElementById("selection-mode");
const btnSelectAll = document.getElementById("btn-select-all");
const btnClear = document.getElementById("btn-clear");
const selectionCountEl = document.getElementById("selection-count");

// Actions
const btnAddTrack = document.getElementById("btn-add-track");
const btnDeleteSelected = document.getElementById("btn-delete-selected");

// =============================================================================
// Async plugin config (shared across all modes)
// =============================================================================

function getAsyncConfig() {
  return {
    adapter: tracksAdapter,
    autoLoad: true,
    storage: {
      chunkSize: CHUNK_SIZE,
      maxCachedItems: 2000,
    },
    loading: {
      cancelThreshold: 8,
      preloadThreshold: 2,
      preloadAhead: 25,
    },
  };
}

// =============================================================================
// Create List — dispatches to the correct view builder
// =============================================================================

function createList(selectionMode) {
  if (list) {
    list.destroy();
  }

  loadRequests = 0;
  loadedCount = 0;

  const container = document.getElementById("list-container");
  container.innerHTML = "";

  if (currentLayoutMode === "grid") {
    createGridView(selectionMode);
  } else if (currentLayoutMode === "table") {
    createTableView(selectionMode);
  } else {
    createListView(selectionMode);
  }

  bindListEvents();
  updateInfo();
  updateContext();
}

// =============================================================================
// Apply scrollbar feature to builder if enabled
// =============================================================================

function applyScrollbar(builder) {
  if (currentScrollbarEnabled) {
    builder.use(
      withScrollbar({
        autoHide: true,
        autoHideDelay: 1000,
        showOnHover: true,
        showOnViewportEnter: true,
      }),
    );
  }
  return builder;
}

// =============================================================================
// Apply scale feature to builder if enabled
// =============================================================================

function applyScale(builder) {
  if (currentScaleEnabled) {
    builder.use(withScale({ force: true }));
  }
  return builder;
}

// =============================================================================
// List View (default — vertical list with 80px rows)
// =============================================================================

function createListView(selectionMode) {
  const builder = vlist({
    container: "#list-container",
    ariaLabel: "Track list",
    item: {
      height: ITEM_HEIGHT,
      template: trackTemplate,
    },
  });

  builder.use(withAsync(getAsyncConfig()));
  applyScale(builder);
  applyScrollbar(builder);
  builder.use(withSelection({ mode: selectionMode }));

  list = builder.build();
}

// =============================================================================
// Grid View (withGrid — card layout)
// =============================================================================

function createGridView(selectionMode) {
  const container = document.getElementById("list-container");
  const innerWidth = container.clientWidth - 2;
  const colWidth = (innerWidth - (GRID_COLUMNS - 1) * GRID_GAP) / GRID_COLUMNS;
  const cardHeight = Math.round(colWidth * 1.3);

  const builder = vlist({
    container: "#list-container",
    ariaLabel: "Track list",
    item: {
      height: (_index, ctx) =>
        ctx ? Math.round(ctx.columnWidth * 1.3) : cardHeight,
      template: trackGridTemplate,
    },
  });

  builder.use(withAsync(getAsyncConfig()));
  builder.use(withGrid({ columns: GRID_COLUMNS, gap: GRID_GAP }));
  applyScale(builder);
  applyScrollbar(builder);
  builder.use(withSelection({ mode: selectionMode }));

  list = builder.build();
}

function createTableView(selectionMode) {
  const builder = vlist({
    container: "#list-container",
    ariaLabel: "Track list",
    item: {
      height: TABLE_ROW_HEIGHT,
      striped: "odd",
      template: trackTableRowTemplate,
    },
  });

  builder.use(withAsync(getAsyncConfig()));
  builder.use(
    withTable({
      columns: trackTableColumns,
      rowHeight: TABLE_ROW_HEIGHT,
      headerHeight: TABLE_HEADER_HEIGHT,
      resizable: true,
      columnBorders: false,
      rowBorders: false,
      minColumnWidth: 50,
    }),
  );
  applyScale(builder);
  applyScrollbar(builder);
  builder.use(withSelection({ mode: selectionMode }));

  list = builder.build();
}

// =============================================================================
// Table View (withTable — columns with header)
// =============================================================================

function createTableView(selectionMode) {
  const builder = vlist({
    container: "#list-container",
    ariaLabel: "Track list",
    item: {
      height: TABLE_ROW_HEIGHT,
      striped: "odd",
      template: trackTableRowTemplate,
    },
  });

  builder.use(withAsync(getAsyncConfig()));
  builder.use(
    withTable({
      columns: trackTableColumns,
      rowHeight: TABLE_ROW_HEIGHT,
      headerHeight: TABLE_HEADER_HEIGHT,
      resizable: true,
      columnBorders: false,
      rowBorders: false,
      minColumnWidth: 50,
    }),
  );
  builder.use(withSelection({ mode: selectionMode }));

  list = builder.build();
}

// =============================================================================
// Layout Mode — List ↔ Grid ↔ Table
// =============================================================================

function setLayoutMode(mode) {
  if (mode === currentLayoutMode) return;
  currentLayoutMode = mode;

  layoutModeEl.querySelectorAll(".ui-segmented__btn").forEach((btn) => {
    btn.classList.toggle(
      "ui-segmented__btn--active",
      btn.dataset.mode === mode,
    );
  });

  // Toggle container class for mode-specific styling
  const container = document.getElementById("list-container");
  container.classList.remove("mode-list", "mode-grid", "mode-table");
  container.classList.add(`mode-${mode}`);

  // Grid/table need more width — add split-main--full
  const splitMain = container.closest(".split-main");
  if (splitMain) {
    splitMain.classList.toggle("split-main--full", mode !== "list");
  }

  createList(currentSelectionMode);
  updateSelectionCount([]);
}

// =============================================================================
// Info bar — right side (contextual)
// =============================================================================

const infoMode = document.getElementById("info-mode");

function updateContext() {
  if (infoMode) infoMode.textContent = currentLayoutMode;
}

layoutModeEl.addEventListener("click", (e) => {
  const btn = e.target.closest("[data-mode]");
  if (btn) setLayoutMode(btn.dataset.mode);
});

// =============================================================================
// Scrollbar Toggle
// =============================================================================

scrollbarToggle.addEventListener("change", (e) => {
  currentScrollbarEnabled = e.target.checked;
  createList(currentSelectionMode);
});

// =============================================================================
// Scale Toggle
// =============================================================================

scaleToggle.addEventListener("change", (e) => {
  currentScaleEnabled = e.target.checked;
  // Scale forces custom scrollbar — lock the toggle on when scale is active
  if (currentScaleEnabled) {
    scrollbarToggle.checked = true;
    scrollbarToggle.disabled = true;
    currentScrollbarEnabled = true;
  } else {
    scrollbarToggle.disabled = false;
  }
  createList(currentSelectionMode);
});

// =============================================================================
// Selection
// =============================================================================

function setSelectionMode(mode) {
  if (mode === currentSelectionMode) return;
  currentSelectionMode = mode;

  selectionModeEl.querySelectorAll(".ui-segmented__btn").forEach((btn) => {
    btn.classList.toggle(
      "ui-segmented__btn--active",
      btn.dataset.value === mode,
    );
  });

  createList(mode);
  updateSelectionCount([]);
}

selectionModeEl.addEventListener("click", (e) => {
  const btn = e.target.closest(".ui-segmented__btn");
  if (btn) setSelectionMode(btn.dataset.value);
});

btnSelectAll.addEventListener("click", () => {
  if (currentSelectionMode !== "multiple") {
    setSelectionMode("multiple");
    // List was recreated — wait for async data to load before selecting
    const unsub = list.on("load:end", () => {
      unsub();
      list.selectAll();
    });
  } else {
    list.selectAll();
  }
});

btnClear.addEventListener("click", () => {
  list.clearSelection();
});

function updateSelectionCount(selected) {
  selectionCountEl.textContent = formatSelectionCount(selected.length);
  btnDeleteSelected.disabled = selected.length === 0;
}

// =============================================================================
// CRUD Operations
// =============================================================================

btnAddTrack.addEventListener("click", async () => {
  const title = prompt("Track title:");
  if (!title) return;

  const artist = prompt("Artist name:");
  if (!artist) return;

  const year = prompt("Year (optional):");
  const country = prompt("Country code (optional, e.g., USA):");

  const trackData = {
    title,
    artist,
    year: year ? parseInt(year, 10) : null,
    country: country || null,
  };

  try {
    const response = await fetch(API_BASE, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(trackData),
    });

    if (!response.ok) throw new Error("Failed to create track");

    alert("Track created successfully!");
    loadedCount = 0;
    await list.reload();
  } catch (error) {
    alert("Failed to create track: " + error.message);
  }
});

const MAX_DELETE = 25;

async function deleteSelected() {
  const selected = list.getSelected();
  if (selected.length === 0) return;

  if (selected.length > MAX_DELETE) {
    alert(
      `You can delete up to ${MAX_DELETE} tracks at a time to keep the demo functional.`,
    );
    return;
  }

  const items = list.getSelectedItems();

  // Delete from server
  const promises = items.map((track) =>
    fetch(`${API_BASE}/${track.id}`, { method: "DELETE" }),
  );

  await Promise.all(promises);

  // Remove each item from the list, tracking the lowest index for auto-select.
  // We delete from highest index to lowest so earlier deletions don't shift
  // the indices of later ones.
  let lowestDeletedIndex = Infinity;

  // Build id→index map before any deletion
  const deleteOrder = items
    .map((track) => {
      const container = list.element;
      const el = container?.querySelector(`[data-id="${track.id}"]`);
      const index = el ? parseInt(el.dataset.index, 10) : -1;
      return { track, index };
    })
    .filter((e) => e.index >= 0)
    .sort((a, b) => b.index - a.index); // highest index first

  deleteOrder.forEach(({ track, index }) => {
    const result = list.removeItem(track.id);
    if (result && index < lowestDeletedIndex) {
      lowestDeletedIndex = index;
    }
  });

  totalTracks = list.total;
  list.clearSelection();
  updateInfo();

  // Auto-select the item that is now at the deleted position
  if (
    list.total > 0 &&
    currentSelectionMode !== "none" &&
    lowestDeletedIndex < Infinity
  ) {
    const targetIndex = Math.min(lowestDeletedIndex, list.total - 1);

    requestAnimationFrame(() => {
      const container = list.element;
      const el = container?.querySelector(`[data-index="${targetIndex}"]`);
      const id = el?.dataset.id;

      if (id && !id.startsWith("__placeholder_")) {
        const numId = Number(id);
        const selectId = Number.isFinite(numId) ? numId : id;
        list.select(selectId);
      }
    });
  }
}

btnDeleteSelected.addEventListener("click", deleteSelected);

// Keyboard shortcut: Delete/Backspace to delete selected item
document.addEventListener("keydown", (e) => {
  if (e.key === "Delete" || e.key === "Backspace") {
    // Don't trigger if user is typing in an input
    if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
    e.preventDefault();
    deleteSelected();
  }
});

// =============================================================================
// Event Bindings
// =============================================================================

function bindListEvents() {
  list.on("selection:change", ({ selected }) => {
    updateSelectionCount(selected);
  });

  list.on("load:end", ({ items, total }) => {
    totalTracks = total;
    updateInfo();
  });

  list.on("scroll", updateInfo);

  list.on("range:change", updateInfo);

  list.on("velocity:change", ({ velocity }) => {
    stats.onVelocity(velocity);
    updateInfo();
  });
}

// =============================================================================
// Initialize
// =============================================================================

async function init() {
  createList(currentSelectionMode);
}

init();
/* Track List — Music playlist styles using design tokens */

/* ============================================================================
   List Container
   ============================================================================ */

#list-container {
    height: 600px;
    overflow: hidden;
}

.ui-segmented {
    flex: none;
}

/* ============================================================================
   Track Item Styles
   ============================================================================ */

.vlist-item {
    border-bottom: 1px solid var(--border);
    transition: background-color 0.2s ease;
}

.vlist-item.vlist-item--selected {
    background-color: var(--badge-error-bg);
}

.vlist-item.vlist-item--selected:hover {
    background-color: var(--badge-error-bg);
    opacity: 0.8;
}

.item-content {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 6px 12px;
    height: 100%;
    overflow: hidden;
}

/* Album artwork container */
.item-artwork {
    width: 36px;
    height: 36px;
    border-radius: var(--radius-xs);
    flex: 0 0 36px;
    overflow: hidden;
    position: relative;
}

/* Cover image (list mode) */
.item-cover {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    border-radius: var(--radius-xs);
    background: var(--vlist-placeholder-bg);
    opacity: 1;
    transition: opacity 0.4s ease;
}

.item-cover.item-cover--loaded {
    opacity: 1;
}

/* Initials fallback (list mode) */
.item-cover--fallback {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--vlist-placeholder-bg);
    color: var(--text);
    font-weight: 600;
    font-size: 13px;
    border-radius: var(--radius-xs);
}

/* Track details */
.item-details {
    flex: 1 1 0%;
    min-width: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.item-title {
    font-weight: 500;
    font-size: var(--fs-sm);
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.3;
}

.item-artist {
    font-size: var(--fs-xs);
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.3;
}

.item-meta {
    font-size: var(--fs-xs);
    color: var(--text-dim);
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.item-separator {
    color: var(--text-dim);
}

.item-year,
.item-country,
.item-duration {
    color: var(--text-dim);
}

/* Duration (right side) */
.item-duration-main {
    flex: 0 0 auto;
    font-size: var(--fs-xs);
    color: var(--text-dim);
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Three-dot menu */
.item-menu {
    flex: 0 0 auto;
    opacity: 0;
    transition: opacity 0.15s ease;
    color: var(--text-muted);
    cursor: pointer;
    padding: 2px;
    font-size: var(--fs-xs);
}

.vlist-item:hover .item-menu {
    opacity: 1;
}

.item-menu:hover {
    color: var(--text);
}

/* ============================================================================
   Grid Mode — Card Layout
   ============================================================================ */

.vlist-grid-item.vlist-item--selected {
    background-color: transparent !important;
}

.vlist-grid-item.vlist-item--selected .grid-card {
    border: 1px solid var(--accent);
    background-color: var(--badge-error-bg);
}

.grid-card {
    display: flex;
    flex-direction: column;
    height: 100%;
    width: 100%;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--surface);
    border: 1px solid var(--border);
    transition: border-color 0.2s ease;
}

.grid-card:hover {
    border-color: var(--text-muted);
}

.vlist-item--selected .grid-card {
    border-color: var(--badge-error-bg);
    box-shadow: 0 0 0 1px var(--badge-error-bg);
}

.grid-card__artwork {
    flex: 1;
    min-height: 0;
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent);
}

/* Cover image (grid mode) */
.grid-cover {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    opacity: 0;
    transition: opacity 0.4s ease;
}

.grid-cover.grid-cover--loaded {
    opacity: 1;
}

/* Initials fallback (grid mode) */
.grid-cover--fallback {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text);
    font-weight: 700;
    font-size: 28px;
    letter-spacing: 1px;
}

.grid-card__body {
    padding: 8px 10px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    border-top: 1px solid var(--border);
}

.grid-card__title {
    font-weight: 500;
    font-size: var(--fs-sm);
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.3;
}

.grid-card__artist {
    font-size: var(--fs-xs);
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.3;
}

.grid-card__meta {
    display: flex;
    gap: 6px;
    font-size: var(--fs-xs);
    color: var(--text-dim);
    margin-top: 2px;
}

/* Grid mode: remove list-style borders on items */
.mode-grid .vlist-item {
    border-bottom: none;
}

/* ============================================================================
   Table Mode — Column Cells
   ============================================================================ */

.table-cell--title {
    font-weight: 500;
}

/* Table mode: tighter selection highlight */
.mode-table .vlist-item.vlist-item--selected {
    background-color: var(--badge-error-bg);
}

/* ============================================================================
   Placeholder skeleton — driven by .vlist-item--placeholder on the wrapper.
   The template is identical for real and placeholder items; mask characters
   (x) set the natural width, CSS hides them and shows skeleton blocks.
   ============================================================================ */

/* List mode placeholders */

.vlist-item--placeholder .item-title {
    color: transparent;
    background-color: var(--vlist-placeholder-bg);
    border-radius: 4px;
    width: fit-content;
    line-height: 1.1;
    margin-bottom: 1px;
}

.vlist-item--placeholder .item-artist {
    color: transparent;
    background-color: var(--vlist-placeholder-bg);
    border-radius: 4px;
    width: fit-content;
    line-height: 1;
}

.vlist-item--placeholder .item-duration-main {
    color: transparent;
}

.vlist-item--placeholder .item-menu {
    visibility: hidden;
}

/* Grid mode placeholders */
.vlist-item--placeholder .grid-cover--fallback {
    color: transparent;
}

.vlist-item--placeholder .grid-card__title {
    color: transparent;
    background-color: var(--vlist-placeholder-bg);
    border-radius: 4px;
    width: fit-content;
    line-height: 1.1;
    margin-bottom: 1px;
}

.vlist-item--placeholder .grid-card__artist {
    color: transparent;
    background-color: var(--vlist-placeholder-bg);
    border-radius: 4px;
    width: fit-content;
    line-height: 1;
}

.vlist-item--placeholder .grid-card__meta {
    visibility: hidden;
}

/* ============================================================================
   Responsive
   ============================================================================ */

@media (max-width: 820px) {
    #list-container {
        height: 400px;
    }

    .item-artwork {
        width: 30px;
        height: 30px;
        flex-basis: 30px;
    }

    .item-cover--fallback {
        font-size: 11px;
    }

    .item-meta {
        display: none;
    }
}
<div class="container">
    <header>
        <h1>Track List</h1>
        <p class="description">
            SQLite-backed music database with CRUD operations
        </p>
    </header>

    <div class="split-layout">
        <div class="split-main">
            <h2 class="sr-only">Tracks</h2>
            <div id="list-container"></div>
        </div>

        <aside class="split-panel">
            <!-- Layout -->
            <section class="ui-section">
                <h3 class="ui-title">Layout</h3>

                <div class="ui-row">
                    <label class="ui-label">Mode</label>
                    <div class="ui-segmented" id="layout-mode">
                        <button
                            class="ui-segmented__btn ui-segmented__btn--active"
                            data-mode="list"
                        >
                            List
                        </button>
                        <button class="ui-segmented__btn" data-mode="grid">
                            Grid
                        </button>
                        <button class="ui-segmented__btn" data-mode="table">
                            Table
                        </button>
                    </div>
                </div>

                <div class="ui-row">
                    <label class="ui-label">Custom Scrollbar</label>
                    <label class="ui-switch">
                        <input type="checkbox" id="scrollbar-toggle" />
                        <span class="ui-switch__track"></span>
                    </label>
                </div>

                <div class="ui-row">
                    <label class="ui-label">Scale</label>
                    <label class="ui-switch">
                        <input type="checkbox" id="scale-toggle" />
                        <span class="ui-switch__track"></span>
                    </label>
                </div>
            </section>

            <!-- Selection -->
            <section class="ui-section">
                <h3 class="ui-title">Selection</h3>

                <div class="ui-row">
                    <label class="ui-label">Mode</label>
                    <div class="ui-segmented" id="selection-mode">
                        <button
                            class="ui-segmented__btn ui-segmented__btn--active"
                            data-value="single"
                        >
                            single
                        </button>
                        <button class="ui-segmented__btn" data-value="multiple">
                            multiple
                        </button>
                    </div>
                </div>

                <div class="ui-row">
                    <label class="ui-label">Actions</label>
                    <div class="ui-btn-group">
                        <button id="btn-select-all" class="ui-btn">
                            Select all
                        </button>
                        <button id="btn-clear" class="ui-btn">Clear</button>
                    </div>
                </div>

                <div class="ui-row">
                    <span class="ui-label">Selected</span>
                    <span class="ui-value" id="selection-count">0 tracks</span>
                </div>
            </section>

            <!-- CRUD Operations -->
            <section class="ui-section">
                <h3 class="ui-title">Operations</h3>

                <div class="ui-row">
                    <button id="btn-add-track" class="ui-btn ui-btn--primary">
                        Add Track
                    </button>
                </div>

                <div class="ui-row">
                    <button
                        id="btn-delete-selected"
                        class="ui-btn ui-btn--danger"
                        disabled
                    >
                        Delete Selected
                    </button>
                </div>
            </section>
        </aside>
    </div>

    <div class="example-info" id="example-info">
        <div class="example-info__left">
            <span class="example-info__stat">
                <strong id="info-progress">0%</strong>
            </span>
            <span class="example-info__stat">
                <span id="info-velocity">0.00</span> /
                <strong id="info-velocity-avg">0.00</strong>
                <span class="example-info__unit">px/ms</span>
            </span>
            <span class="example-info__stat">
                <span id="info-dom">0</span> /
                <strong id="info-total">0</strong>
                <span class="example-info__unit">items</span>
            </span>
        </div>
        <div class="example-info__right">
            <span class="example-info__stat">
                <strong id="info-mode">list</strong>
            </span>
        </div>
    </div>
</div>