/ Examples
gridgroupsscrollbar

File Browser

Finder-like file browser using vlist/builder with withGrid plugin. Browse files from vlist and vlist.dev projects with switchable grid/list views. Click to select, double-click folders to navigate.

File browser demo using withGrid plugin with switchable views. Backend API serves real filesystem data. Click to select, double-click folders to navigate. 📁

Source
// Builder Grid — File Browser
// Uses vlist/builder with withGrid plugin for grid view and standard list for list view
// Demonstrates a virtualized file browser similar to macOS Finder

import { vlist, withGrid, withScrollbar, withGroups } from "vlist";

// =============================================================================
// File Type Icons
// =============================================================================

const FILE_ICONS = {
  folder: "📁",
  js: "📄",
  ts: "📘",
  json: "📋",
  html: "🌐",
  css: "🎨",
  scss: "🎨",
  md: "📝",
  png: "🖼️",
  jpg: "🖼️",
  jpeg: "🖼️",
  gif: "🖼️",
  svg: "🖼️",
  txt: "📄",
  pdf: "📕",
  zip: "📦",
  default: "📄",
};

function getFileIcon(item) {
  if (item.type === "directory") return FILE_ICONS.folder;
  const ext = item.extension;
  return FILE_ICONS[ext] || FILE_ICONS.default;
}

function getFileKind(item) {
  if (item.type === "directory") return "Folder";
  const ext = item.extension;

  const kindMap = {
    js: "JavaScript",
    ts: "TypeScript",
    json: "JSON",
    html: "HTML",
    css: "CSS",
    scss: "SCSS",
    md: "Markdown",
    txt: "Text",
    png: "PNG Image",
    jpg: "JPEG Image",
    jpeg: "JPEG Image",
    gif: "GIF Image",
    svg: "SVG Image",
    pdf: "PDF",
    zip: "Archive",
    gz: "Archive",
    tar: "Archive",
  };

  return kindMap[ext] || (ext ? ext.toUpperCase() : "Document");
}

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

let currentPath = "";
let items = [];
let currentView = "list";
let currentColumns = 6;
let currentGap = 8;
let list = null;
let navigationHistory = [""];
let historyIndex = 0;
let selectedIndex = -1;
let currentArrangeBy = "name";

// =============================================================================
// Templates
// =============================================================================

const gridItemTemplate = (item) => {
  const icon = getFileIcon(item);

  return `
    <div class="file-card">
      <div class="file-card__icon">
        ${icon}
      </div>
      <div class="file-card__name" title="${item.name}">
        ${item.name}
      </div>
    </div>
  `;
};

const listItemTemplate = (item) => {
  const icon = getFileIcon(item);
  const sizeText =
    item.type === "file" && item.size != null ? formatFileSize(item.size) : "—";
  const kind = getFileKind(item);

  // Format date
  const now = new Date();
  const modified = new Date(item.modified);
  const isToday =
    now.getFullYear() === modified.getFullYear() &&
    now.getMonth() === modified.getMonth() &&
    now.getDate() === modified.getDate();

  let dateText = "";
  if (isToday) {
    const timeStr = modified.toLocaleTimeString("en-US", {
      hour: "numeric",
      minute: "2-digit",
      hour12: true,
    });
    dateText = `Today at ${timeStr}`;
  } else {
    const month = modified.toLocaleDateString("en-US", { month: "short" });
    const day = modified.getDate();
    const year = modified.getFullYear();
    dateText = `${month} ${day}, ${year}`;
  }

  return `
    <div class="file-row">
      <div class="file-row__icon">
        ${icon}
      </div>
      <div class="file-row__name">${item.name}</div>
      <div class="file-row__size">${sizeText}</div>
      <div class="file-row__date">${dateText}</div>
      <div class="file-row__kind">${kind}</div>
    </div>
  `;
};

// =============================================================================
// Date Grouping
// =============================================================================

function getDateGroup(item) {
  const now = new Date();
  const modified = new Date(item.modified);

  // Check if today
  if (
    now.getFullYear() === modified.getFullYear() &&
    now.getMonth() === modified.getMonth() &&
    now.getDate() === modified.getDate()
  ) {
    return "Today";
  }

  const diffTime = Math.abs(now - modified);
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

  if (diffDays <= 7) return "Previous 7 Days";
  if (diffDays <= 30) return "Previous 30 Days";
  return "Older";
}

// Get arrangement configuration
function getArrangementConfig(arrangeBy) {
  switch (arrangeBy) {
    case "name":
      return {
        groupBy: "none",
        sortFn: (a, b) => {
          // Folders first
          if (a.type === "directory" && b.type !== "directory") return -1;
          if (a.type !== "directory" && b.type === "directory") return 1;
          // Then alphabetically
          return a.name.localeCompare(b.name, undefined, { numeric: true });
        },
      };
    case "kind":
      return {
        groupBy: "kind",
        sortFn: (a, b) => {
          const kindA = getFileKind(a);
          const kindB = getFileKind(b);
          if (kindA !== kindB) {
            return kindA.localeCompare(kindB);
          }
          // Within same kind, sort by name
          return a.name.localeCompare(b.name, undefined, { numeric: true });
        },
      };
    case "date-modified":
      return {
        groupBy: "date",
        sortFn: (a, b) => {
          const groupA = getDateGroup(a);
          const groupB = getDateGroup(b);
          const groupOrder = [
            "Today",
            "Previous 7 Days",
            "Previous 30 Days",
            "Older",
          ];
          const orderA = groupOrder.indexOf(groupA);
          const orderB = groupOrder.indexOf(groupB);
          if (orderA !== orderB) {
            return orderA - orderB;
          }
          // Within same group, sort by date (newest first)
          const dateA = new Date(a.modified).getTime();
          const dateB = new Date(b.modified).getTime();
          if (isNaN(dateA)) return 1;
          if (isNaN(dateB)) return -1;
          return dateB - dateA;
        },
      };
    case "size":
      return {
        groupBy: "none",
        sortFn: (a, b) => {
          if (a.type === "directory" && b.type !== "directory") return -1;
          if (a.type !== "directory" && b.type === "directory") return 1;
          return (b.size || 0) - (a.size || 0); // Largest first
        },
      };
    default:
      return {
        groupBy: "none",
        sortFn: (a, b) => a.name.localeCompare(b.name),
      };
  }
}

// =============================================================================
// Utility Functions
// =============================================================================

function formatFileSize(bytes) {
  if (bytes === 0) return "0 B";
  const k = 1024;
  const sizes = ["B", "KB", "MB", "GB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}

function formatPath(path) {
  return path || "/";
}

// =============================================================================
// API
// =============================================================================

async function fetchDirectory(path) {
  try {
    const response = await fetch(`/api/files?path=${encodeURIComponent(path)}`);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Failed to fetch directory:", error);
    return { path, items: [] };
  }
}

// =============================================================================
// View Creation
// =============================================================================

async function createBrowser(view = "grid") {
  // Destroy previous
  if (list) {
    list.destroy();
    list = null;
  }

  // Clear container
  const container = document.getElementById("browser-container");
  container.innerHTML = "";

  currentView = view;

  if (view === "grid") {
    createGridView();
  } else {
    createListView();
  }

  updateNavigationState();
}

function createGridView() {
  const container = document.getElementById("browser-container");
  const innerWidth = container.clientWidth - 2;
  const colWidth =
    (innerWidth - (currentColumns - 1) * currentGap) / currentColumns;
  const height = colWidth * 0.8; // Icon + text

  // Get arrangement config (grouping + sorting)
  const config = getArrangementConfig(currentArrangeBy);
  const sorted = [...items].sort(config.sortFn);

  // Create group map if grouping is enabled
  let groupMap = null;
  if (config.groupBy !== "none") {
    groupMap = new Map();
    const groupCounts = {};
    sorted.forEach((item, index) => {
      const groupKey =
        config.groupBy === "date" ? getDateGroup(item) : getFileKind(item);
      groupMap.set(index, groupKey);
      groupCounts[groupKey] = (groupCounts[groupKey] || 0) + 1;
    });
  }

  // Hide list header in grid view
  const listHeader = document.getElementById("list-header");
  if (listHeader) listHeader.style.display = "none";

  // Create list with builder pattern
  let builder = vlist({
    container: "#browser-container",
    ariaLabel: "File browser",
    item: {
      height,
      template: gridItemTemplate,
    },
    items: sorted,
  })
    .use(withGrid({ columns: currentColumns, gap: currentGap }))
    .use(withScrollbar({ autoHide: true }));

  // Add groups plugin if grouping is enabled
  if (config.groupBy !== "none" && groupMap) {
    builder = builder.use(
      withGroups({
        getGroupForIndex: (index) => groupMap.get(index) || "",
        headerHeight: 40,
        headerTemplate: (groupKey) => {
          // Count items in this group
          let count = 0;
          groupMap.forEach((key) => {
            if (key === groupKey) count++;
          });
          return `
          <div class="group-header">
            <span class="group-header__label">${groupKey}</span>
            <span class="group-header__count">${count} items</span>
          </div>
        `;
        },
        sticky: true,
      }),
    );
  }

  list = builder.build();

  // Bind events

  list.on("item:click", ({ item, index }) => {
    handleItemClick(item, index);
  });

  list.on("item:dblclick", ({ item, index }) => {
    if (!item.__groupHeader) {
      handleItemDoubleClick(item);
    }
  });
}

function createListView() {
  const height = 28; // Fixed row height for list view

  // Get arrangement config (grouping + sorting)
  const config = getArrangementConfig(currentArrangeBy);
  const sorted = [...items].sort(config.sortFn);

  // Show list header
  const listHeader = document.getElementById("list-header");
  if (listHeader) listHeader.style.display = "grid";

  // Create group map if grouping is enabled
  let groupMap = null;
  if (config.groupBy !== "none") {
    groupMap = new Map();
    const groupCounts = {};
    sorted.forEach((item, index) => {
      const groupKey =
        config.groupBy === "date" ? getDateGroup(item) : getFileKind(item);
      groupMap.set(index, groupKey);
      groupCounts[groupKey] = (groupCounts[groupKey] || 0) + 1;
    });
    console.log("🔍 List View Grouping Debug:", {
      arrangeBy: currentArrangeBy,
      groupBy: config.groupBy,
      totalItems: sorted.length,
      groupCounts,
      firstTenGroups: Array.from(
        { length: Math.min(10, sorted.length) },
        (_, i) => ({
          index: i,
          name: sorted[i].name,
          type: sorted[i].type,
          kind: getFileKind(sorted[i]),
          dateGroup: getDateGroup(sorted[i]),
          assignedGroup: groupMap.get(i),
        }),
      ),
    });
  }

  // Create list with builder pattern
  let builder = vlist({
    container: "#browser-container",
    ariaLabel: "File browser",
    item: {
      height,
      template: listItemTemplate,
    },
    items: sorted,
  }).use(withScrollbar({ autoHide: true }));

  // Add groups plugin if grouping is enabled
  if (config.groupBy !== "none" && groupMap) {
    builder = builder.use(
      withGroups({
        getGroupForIndex: (index) => groupMap.get(index) || "",
        headerHeight: 40,
        headerTemplate: (groupKey) => {
          // Count items in this group
          let count = 0;
          groupMap.forEach((key) => {
            if (key === groupKey) count++;
          });
          return `
          <div class="group-header">
            <span class="group-header__label">${groupKey}</span>
            <span class="group-header__count">${count} items</span>
          </div>
        `;
        },
        sticky: true,
      }),
    );
  }

  list = builder.build();

  // Bind events

  list.on("item:click", ({ item, index }) => {
    handleItemClick(item, index);
  });

  list.on("item:dblclick", ({ item, index }) => {
    console.log("🖱️🖱️ List dblclick event fired:", {
      item,
      index,
      type: item.type,
      name: item.name,
    });
    if (!item.__groupHeader) {
      console.log("📁 Calling handleItemDoubleClick for:", item.name);
      handleItemDoubleClick(item);
    } else {
      console.log("⚠️ Skipping group header");
    }
  });
}

// =============================================================================
// Navigation
// =============================================================================

async function navigateTo(path, addToHistory = true) {
  const data = await fetchDirectory(path);
  currentPath = data.path;
  items = data.items;

  // Clear selection when navigating
  selectedIndex = -1;

  // Update history
  if (addToHistory) {
    // Remove any forward history
    navigationHistory = navigationHistory.slice(0, historyIndex + 1);
    navigationHistory.push(path);
    historyIndex = navigationHistory.length - 1;
  }

  await createBrowser(currentView);
  updateBreadcrumb();
  updateNavigationState();
}

function handleItemClick(item, index) {
  // Update selection state
  if (selectedIndex >= 0) {
    // Deselect previous
    const prevEl = document.querySelector(`[data-index="${selectedIndex}"]`);
    if (prevEl) prevEl.setAttribute("aria-selected", "false");
  }

  selectedIndex = index;

  // Select current
  const currentEl = document.querySelector(`[data-index="${index}"]`);
  if (currentEl) currentEl.setAttribute("aria-selected", "true");
}

function handleItemDoubleClick(item) {
  if (item.type === "directory") {
    const newPath = currentPath ? `${currentPath}/${item.name}` : item.name;
    selectedIndex = -1; // Clear selection when navigating
    navigateTo(newPath);
  }
}

async function navigateBack() {
  if (historyIndex > 0) {
    historyIndex--;
    await navigateTo(navigationHistory[historyIndex], false);
  }
}

async function navigateForward() {
  if (historyIndex < navigationHistory.length - 1) {
    historyIndex++;
    await navigateTo(navigationHistory[historyIndex], false);
  }
}

// =============================================================================
// UI Updates
// =============================================================================

const breadcrumbEl = document.getElementById("breadcrumb");

function updateBreadcrumb() {
  const parts = currentPath ? currentPath.split("/") : [];
  let html = `<button class="breadcrumb__item" data-path="">home</button>`;
  let pathSoFar = "";

  parts.forEach((part, index) => {
    pathSoFar += (index > 0 ? "/" : "") + part;
    html += `<span class="breadcrumb__sep">›</span>`;
    html += `<button class="breadcrumb__item" data-path="${pathSoFar}">${part}</button>`;
  });

  breadcrumbEl.innerHTML = html;
}

function updateNavigationState() {
  const backBtn = document.getElementById("btn-back");
  const forwardBtn = document.getElementById("btn-forward");
  backBtn.disabled = historyIndex <= 0;
  forwardBtn.disabled = historyIndex >= navigationHistory.length - 1;
}

// =============================================================================
// Initialization
// =============================================================================

(async () => {
  // Set up view switcher
  document.getElementById("btn-view-grid").addEventListener("click", () => {
    if (currentView === "grid") return;
    document.getElementById("btn-view-grid").classList.add("view-btn--active");
    document
      .getElementById("btn-view-list")
      .classList.remove("view-btn--active");
    createBrowser("grid");
  });

  document.getElementById("btn-view-list").addEventListener("click", () => {
    if (currentView === "list") return;
    document.getElementById("btn-view-list").classList.add("view-btn--active");
    document
      .getElementById("btn-view-grid")
      .classList.remove("view-btn--active");
    createBrowser("list");
  });

  // Set up arrange by
  document
    .getElementById("arrange-by-select")
    .addEventListener("change", (e) => {
      currentArrangeBy = e.target.value;
      createBrowser(currentView);
    });

  // Set up navigation
  document.getElementById("btn-back").addEventListener("click", () => {
    navigateBack();
  });

  document.getElementById("btn-forward").addEventListener("click", () => {
    navigateForward();
  });

  // Breadcrumb click handler
  breadcrumbEl.addEventListener("click", (e) => {
    const btn = e.target.closest("[data-path]");
    if (!btn) return;
    navigateTo(btn.dataset.path);
  });

  // Initial load - start in vlist folder with list view
  await navigateTo("vlist");
})();
/* Builder Grid — File Browser example-specific styles
   Common styles (.container, h1, .description, .stats, footer)
   are provided by example/example.css using shell.css design tokens.
   Panel system (.split-layout, .split-panel, .panel-*)
   is also provided by example/example.css. */

/* Browser container */
#browser-container {
    height: 500px;
    margin: 0 auto;
    background: var(--bg);
}

/* Override vlist default styles - remove borders and border-radius */
#browser-container .vlist {
    border: none !important;
    border-radius: 0 !important;
}

#browser-container .vlist-viewport {
    border-radius: 0 !important;
}

/* ============================================================================
   Breadcrumb Navigation
   ============================================================================ */

.breadcrumb {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 0;
    margin: 0;
    border-radius: 0;
    border: none;
    background: transparent;
    font-size: 13px;
    overflow-x: auto;
    white-space: nowrap;
    flex: 1;
    min-width: 0;
}

.breadcrumb__item {
    padding: 4px 8px;
    border: none;
    border-radius: 4px;
    background: transparent;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 500;
    font-family: inherit;
    cursor: pointer;
    transition: all 0.15s ease;
    white-space: nowrap;
}

.breadcrumb__item:hover {
    background: var(--bg-hover);
    color: var(--text);
}

.breadcrumb__item:last-child {
    color: var(--text);
    font-weight: 600;
}

.breadcrumb__sep {
    color: var(--text-muted);
    font-size: 12px;
}

/* ============================================================================
   Toolbar
   ============================================================================ */

.toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 8px 12px;
    margin-bottom: 12px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--bg-card);
}

.toolbar__left {
    display: flex;
    gap: 8px;
    align-items: center;
    flex: 1;
    min-width: 0;
}

.toolbar__right {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-shrink: 0;
}

.toolbar__btn {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font-size: 13px;
    font-weight: 500;
    font-family: inherit;
    cursor: pointer;
    transition: all 0.15s ease;
}

.toolbar__btn--icon {
    padding: 6px;
    min-width: 32px;
    min-height: 32px;
    justify-content: center;
}

.toolbar__btn--icon svg {
    display: block;
    opacity: 0.7;
    transition: opacity 0.15s ease;
}

.toolbar__btn--icon:hover:not(:disabled) svg {
    opacity: 1;
}

.toolbar__btn:hover:not(:disabled) {
    border-color: var(--accent);
    color: var(--accent-text);
}

.toolbar__btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.toolbar__btn .icon {
    font-size: 14px;
}

.toolbar__label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
}

.toolbar__select {
    padding: 4px 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font-size: 12px;
    font-family: inherit;
    cursor: pointer;
    transition: all 0.15s ease;
}

.toolbar__select:hover {
    border-color: var(--accent);
}

.toolbar__select:focus {
    outline: none;
    border-color: var(--accent);
}

/* ============================================================================
   View Switcher
   ============================================================================ */

.view-switcher {
    display: flex;
    gap: 4px;
    padding: 4px;
    border-radius: 8px;
    background: var(--bg);
    border: 1px solid var(--border);
}

.view-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 32px;
    border: none;
    border-radius: 6px;
    background: transparent;
    color: var(--text-muted);
    font-size: 16px;
    cursor: pointer;
    transition: all 0.15s ease;
}

.view-btn:hover {
    background: var(--bg-hover);
    color: var(--text);
}

.view-btn--active {
    background: var(--accent);
    color: white;
}

/* ============================================================================
   Control Buttons (columns / gap)
   ============================================================================ */

.ctrl-btn {
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg-card);
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: all 0.15s ease;
    min-width: 36px;
    text-align: center;
}

.ctrl-btn:hover {
    border-color: var(--accent);
    color: var(--accent-text);
}

.ctrl-btn--active {
    background: var(--accent);
    color: white;
    border-color: var(--accent);
}

/* ============================================================================
   File Card (Grid View)
   ============================================================================ */

.file-card {
    position: relative;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 8px 6px;
    border-radius: 8px;
    background: transparent;
    cursor: pointer;
    transition: all 0.2s ease;
}

.file-card:hover {
    background: transparent;
}

.file-card[data-type="directory"]:hover {
    background: transparent;
}

/* Finder-style selection - no card background */
.vlist-item[aria-selected="true"] .file-card {
    background: transparent;
}

.vlist-item[aria-selected="true"] .file-card__name {
    background: rgba(0, 122, 255, 0.8);
    color: white;
    padding: 2px 6px;
    border-radius: 4px;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

.file-card__icon {
    font-size: 52px;
    line-height: 1;
    margin-bottom: 6px;
}

.file-card__name {
    font-weight: 500;
    color: var(--text);
    text-align: center;
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: 1.3;
    max-height: 2.6em;
    word-break: break-word;
}

/* ============================================================================
   List View Header
   ============================================================================ */

.list-header {
    display: grid;
    grid-template-columns: 24px minmax(200px, 2fr) 100px 200px 140px;
    align-items: center;
    gap: 12px;
    padding: 0 12px;
    height: 24px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    background: transparent;
    font-size: 11px;
    font-weight: 500;
    color: var(--text-muted);
    margin-bottom: 0;
}

.list-header__icon {
    width: 24px;
}

.list-header__name,
.list-header__size,
.list-header__date,
.list-header__kind {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.list-header__size {
    text-align: right;
}

/* ============================================================================
   Manual Grid with Groups
   ============================================================================ */

.manual-grid-groups {
    width: 100%;
}

.manual-grid {
    display: grid;
    padding: 12px 0;
    margin-bottom: 24px;
}

.manual-grid-item {
    cursor: pointer;
}

.manual-grid-item[aria-selected="true"] .file-card {
    background: transparent;
}

.manual-grid-item[aria-selected="true"] .file-card__name {
    background: rgba(0, 122, 255, 0.8);
    color: white;
    padding: 2px 6px;
    border-radius: 4px;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

/* ============================================================================
   Group Headers
   ============================================================================ */

.group-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 12px 8px 48px;
    background: transparent;
    margin-top: 12px;
    transition: all 0.2s ease;
}

/* Group headers in grid layout - background and spacing */
.vlist--grid .group-header {
    background: rgba(255, 255, 255, 0.03);
    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
    margin-top: 0;
    margin-bottom: 8px;
}

/* First group header has no margin-top */
.vlist-item:first-child .group-header {
    margin-top: 0;
}

/* Sticky header enhancement */
.vlist-item[data-sticky="true"] .group-header {
    background: rgba(15, 23, 42, 0.95);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    z-index: 10;
}

.group-header__label {
    font-size: 13px;
    font-weight: 600;
    color: var(--text);
    text-transform: uppercase;
    letter-spacing: 0.8px;
}

.group-header__count {
    font-size: 11px;
    color: var(--text-muted);
    opacity: 0.6;
}

/* Enhanced count visibility when sticky */
.vlist-item[data-sticky="true"] .group-header__count {
    opacity: 0.8;
    background: rgba(255, 255, 255, 0.1);
    padding: 2px 8px;
    border-radius: 10px;
}

/* ============================================================================
   File Row (List View)
   ============================================================================ */

.file-row {
    font-size: 14px;
    display: grid;
    grid-template-columns: 24px minmax(200px, 2fr) 100px 200px 140px;
    align-items: center;
    gap: 12px;
    width: 100%;
    height: 100%;
    padding: 0 12px;
    background: transparent;
    border-radius: 0 !important;
    cursor: pointer;
    transition: background 0.1s ease;
}

/* Zebra striping (even rows) */
.vlist-item:nth-child(even) .file-row {
    background: rgba(255, 255, 255, 0.02);
}

/* Finder-style selection for list view */
.vlist-item[aria-selected="true"] .file-row {
    background: rgba(0, 122, 255, 0.5);
}

.vlist-item[aria-selected="true"] .file-row:hover {
    background: rgba(0, 122, 255, 0.6);
}

.file-row__icon {
    font-size: 18px;
    line-height: 1;
    text-align: center;
}

.file-row__name {
    font-weight: 400;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.vlist-item[aria-selected="true"] .file-row__name,
.vlist-item[aria-selected="true"] .file-row__size,
.vlist-item[aria-selected="true"] .file-row__date,
.vlist-item[aria-selected="true"] .file-row__kind {
    color: white;
}

.file-row__size {
    color: var(--text-muted);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.file-row__date {
    color: var(--text-muted);
}

.file-row__kind {
    color: var(--text-muted);
}

/* ============================================================================
   File Detail (panel)
   ============================================================================ */

.detail__meta {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 12px;
}

.detail__meta strong {
    font-weight: 600;
    font-size: 13px;
    margin-bottom: 4px;
}

.detail__meta span {
    font-size: 12px;
    color: var(--text-muted);
}

/* ============================================================================
   Panel value styling
   ============================================================================ */

.panel-value {
    font-size: 12px;
    color: var(--text-muted);
    word-break: break-all;
}

/* ============================================================================
   vlist overrides — remove item borders/padding for cards
   ============================================================================ */

#browser-container .vlist-item {
    padding: 0;
    border: none;
    background: transparent;
}

#browser-container .vlist-item:hover {
    background: transparent;
}

/* List view specific - no padding, no border, no border-radius */
#browser-container .vlist-item:has(.file-row) {
    padding: 0;
    border: none !important;
    border-radius: 0 !important;
}

/* Group headers in list view - full width, no grid layout */
#browser-container .vlist-item:has(.group-header) {
    padding: 0;
    border: none !important;
    border-radius: 0 !important;
}

#browser-container .vlist-item:has(.group-header) .group-header {
    width: 100%;
}

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

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

    .breadcrumb {
        font-size: 12px;
    }

    .toolbar {
        flex-direction: column;
        gap: 8px;
    }

    .toolbar__left,
    .toolbar__right {
        width: 100%;
        justify-content: space-between;
    }

    .list-header {
        grid-template-columns: 24px 1fr 80px;
        gap: 8px;
    }

    .list-header__date,
    .list-header__kind {
        display: none;
    }

    .file-row {
        grid-template-columns: 24px 1fr 80px;
        gap: 8px;
    }

    .file-row__date,
    .file-row__kind {
        display: none;
    }

    .file-card__icon {
        font-size: 44px;
    }
}
<div class="container container">
    <header>
        <h1>File Browser</h1>
        <p class="description">
            Finder-like file browser using <code>vlist/builder</code> with
            <code>withGrid</code> plugin. Browse files from vlist and vlist.dev
            projects with switchable grid/list views. Click to select,
            double-click folders to navigate.
        </p>
    </header>

    <!-- Toolbar -->
    <div class="toolbar">
        <div class="toolbar__left">
            <button
                id="btn-back"
                class="toolbar__btn toolbar__btn--icon"
                disabled
                title="Back"
            >
                <svg
                    xmlns="http://www.w3.org/2000/svg"
                    height="20px"
                    viewBox="0 0 24 24"
                    width="20px"
                    fill="currentColor"
                >
                    <path d="M0 0h24v24H0z" fill="none" />
                    <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
                </svg>
            </button>
            <button
                id="btn-forward"
                class="toolbar__btn toolbar__btn--icon"
                disabled
                title="Forward"
            >
                <svg
                    xmlns="http://www.w3.org/2000/svg"
                    height="20px"
                    viewBox="0 0 24 24"
                    width="20px"
                    fill="currentColor"
                >
                    <path d="M0 0h24v24H0z" fill="none" />
                    <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
                </svg>
            </button>
            <!-- Breadcrumb navigation -->
            <div class="breadcrumb" id="breadcrumb">
                <button class="breadcrumb__item" data-path="">Home</button>
            </div>
        </div>
        <div class="toolbar__right">
            <select id="arrange-by-select" class="toolbar__select">
                <option value="name" selected>Name</option>
                <option value="kind">Kind</option>
                <option value="date-modified">Date Modified</option>
                <option value="size">Size</option>
            </select>
            <div class="view-switcher">
                <button id="btn-view-grid" class="view-btn" title="Grid View">
                    <span class="icon">⊞</span>
                </button>
                <button
                    id="btn-view-list"
                    class="view-btn view-btn--active"
                    title="List View"
                >
                    <span class="icon">☰</span>
                </button>
            </div>
        </div>
    </div>

    <!-- List view header (hidden in grid view) -->
    <div class="list-header" id="list-header" style="display: none">
        <div class="list-header__icon"></div>
        <div class="list-header__name">Name</div>
        <div class="list-header__size">Size</div>
        <div class="list-header__date">Date Modified</div>
        <div class="list-header__kind">Kind</div>
    </div>

    <!-- File browser container -->
    <div id="browser-container"></div>

    <footer>
        <p>
            File browser demo using <code>withGrid</code> plugin with switchable
            views. Backend API serves real filesystem data. Click to select,
            double-click folders to navigate. 📁
        </p>
    </footer>
</div>