> ## Documentation Index
> Fetch the complete documentation index at: https://ej3mplo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Cover grid

> Draw the library fast, even with thousands of games.

ejGames ships an optional **kit** of utilities in `/_sdk/kit/`. They are imported
with paths that start with `/_sdk/…`. Your own files use relative paths
(`./util.js`). For the grid we will use these pieces from `dom.js` and `library.js`:

* **`h(tag, props, ...children)`** creates elements without writing
  `createElement` every time. `class` sets the class, `onclick` adds the event, and
  the rest (`title`, `data-…`) is set as an attribute. `style` can be a string
  (`"--tono: 120"`) or an object (`{ opacity: "0.5" }`). For CSS variables, use
  the string.
* **`img(url)`** creates an `<img>` with lazy loading (`loading="lazy"`). It adds
  the class `img-loaded` when it loads, `img-error` if it fails, and `img-empty`
  if there was no URL, so CSS decides what to show in each case.
* **`keyed(container, list, key, paintOne)`** redraws by reusing the nodes that
  already existed. `paintOne(game, previous)` receives that game's previous node
  (or nothing) and returns the one to use. With hundreds of games it stays fast,
  and focus does not jump.
* **`visible(games)`** removes hidden games, missing games, emulators and console
  games, which `library.all` does include (those belong in [Homebrew](https://ej3mplo.mintlify.site/referencia/homebrew.md)), and **`sort(games, "title")`** sorts (`"title"`, `"recent"`,
  `"playtime"`, `"added"`, `"release"` or `"rating"`).

Replace `main.js` with this:

```js theme={null}
import { h, img, keyed, initials, hueOf } from "/_sdk/kit/dom.js";
import { visible, sort } from "/_sdk/kit/library.js";

const ejg = await window.ejg.ready();
const rejilla = document.querySelector("#rejilla");
const total = document.querySelector("#total");

function juegos() {
  return sort(visible(ejg.library.all), "title");
}

function nuevaTarjeta(g, firma) {
  return h(
    "button",
    { class: "tarjeta", "data-id": g.id, "data-firma": firma, title: g.title, onclick: () => jugar(g.id) },
    h(
      "div",
      { class: "caratula", style: `--tono: ${hueOf(g.title)}` },
      h("span", { class: "iniciales" }, initials(g.title)),
      img(g.media.coverThumb),
    ),
    h("span", { class: "nombre" }, g.title),
  );
}

function tarjeta(g, prev) {
  // A new card is created only when what you see changes (name or cover).
  const firma = `${g.title}|${g.media.coverThumb ?? ""}`;
  const el = prev && prev.dataset.firma === firma ? prev : nuevaTarjeta(g, firma);
  el.classList.toggle("fav", g.favorite);
  return el;
}

function pintar() {
  const lista = juegos();
  total.textContent = lista.length;
  if (!lista.length) {
    rejilla.replaceChildren(
      h("div", { class: "vacia" },
        h("p", null, "Tu biblioteca está vacía."),
        h("button", { class: "boton", onclick: () => ejg.ui.open("add-folder") }, "Añadir juegos")),
    );
    return;
  }
  keyed(rejilla, lista, (g) => g.id, tarjeta);
}

async function jugar(id) {
  try {
    await ejg.game.launch(id);
  } catch (e) {
    ejg.ui.toast(String(e.message || e), "error");
  }
}

ejg.library.onChange(pintar);
pintar();
```

A few ideas in this code:

* **Thumbnails in the grid.** `coverThumb` is about 40 KB. `cover` is the large
  image. In lists and grids, always use thumbnails.
* **The "signature"** is the trick for not rebuilding cards. If the name and the
  cover are the same, the previous card is reused and only the classes change
  (favorite). When the cover for a newly added game arrives, the signature
  changes and a new card is created.
* **With no cover** you see the initials (`initials("Hollow Knight")` → `HK`) on
  a gradient whose color comes from the name (`hueOf()`), and the image stays
  invisible until it loads.
* **`ejg.game.launch(id)`** launches the game (ejGames plays the launch sound).
  If it fails, the promise rejects and we show it with **`ejg.ui.toast(text, "error")`**.
* **`ejg.ui.open("add-folder")`** opens the launcher screen for adding game
  folders. There are more: `"settings"`, `"search"`, `"menu"`, `"stats"`,
  `"profiles"`, `"collections"`, `"theme"`…

And the grid CSS (add it to `style.css`):

```css theme={null}
.rejilla {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 4px 24px 90px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 10px;
  align-content: start;
}
.tarjeta {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  padding: 8px;
  /* With hundreds of games, the browser does not paint the ones off screen. */
  content-visibility: auto;
  contain-intrinsic-size: auto 270px;
}
.caratula {
  position: relative;
  aspect-ratio: 2 / 3;
  border-radius: 10px;
  overflow: hidden;
  background: linear-gradient(160deg, hsl(var(--tono) 45% 32%), hsl(var(--tono) 50% 14%));
  transition: transform 0.15s, box-shadow 0.15s;
}
.iniciales { position: absolute; inset: 0; display: grid; place-items: center; font-size: 2.2em; font-weight: 700; opacity: 0.75; }
.caratula img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.25s; }
.caratula img.img-loaded { opacity: 1; }
.nombre { font-size: 0.9em; color: var(--tenue); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.tarjeta.fav .caratula::after { content: "★"; position: absolute; top: 6px; right: 9px; color: #ffd166; text-shadow: 0 1px 4px #000; }
.vacia { grid-column: 1 / -1; padding: 60px 0; text-align: center; color: var(--tenue); }
```

> [!WARNING]
>
> **`content-visibility: auto`** saves a lot with large libraries, but it **clips
> anything that sticks out of the card** (shadows, focus rings, zooms). That is
> why the card has `padding: 8px`: it leaves room for the focus effect in the next
> step.

Save. You now have a cover grid, and a click launches the game.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.