Skip to content
Pablo Martínez López edited this page Oct 29, 2026 · 1 revision

Icons

English · Español

Every icon of the app is an Iconify icon drawn by one component, <Icono icon="prefix:name">. There are two kinds: the ones the code writes literally (bundled into the app at build time, painted at once, no network) and the ones the user picks with the icon picker (for now, the icon of a library tag), which Rust downloads once from the Iconify API and keeps on disk so they show offline.

The only exception is src/components/ui/: the shadcn/judlex primitives are read-only and keep importing lucide-react (chevrons of selects and menus, the close cross of dialogs, the check of checkboxes). Everything outside that folder uses <Icono>.

Files

File Responsibility
src/components/icono.tsx Icono({ icon, size, ... }): an <svg> with the icon's viewBox and body. Bundled icons come from INCLUIDOS; any other id is asked for through useIconos().pedir(id) and its box (24 px by default, or the size-* class) is kept empty until it arrives, so nothing jumps. An empty id paints nothing.
src/lib/iconos/incluidos.ts Generated by scripts/iconos.mjs (never edited by hand, excluded from Biome): INCLUIDOS: Record<id, { body, vb }>, the data of every icon id written literally in src/.
scripts/iconos.mjs Walks src/**/*.{ts,tsx} (skipping node_modules, bindings, assets, __tests__ and test files), finds every quoted prefix:name, looks it up in the @iconify/json collections (aliases resolved with getIconData/iconToSVG from @iconify/utils) and writes incluidos.ts. Quoted strings that are not icons (16:9, pbimg:…) drop out because their prefix is not a collection; an id whose collection exists but whose name does not is an error. --check fails if the file is not up to date.
src/components/selector-icono.tsx SelectorIcono({ valor, onChange, quitable }): the icon picker.
src/stores/iconos-store.ts useIconos: datos (the icons Rust already returned in this session), pedir(id) (batches the ids asked for during one paint, 16 ms, into a single iconos_datos; on failure the ids can be asked for again) and recibir.
src-tauri/src/iconos.rs The Iconify client: catalogue, search, icon data, chosen icons, SVG sanitising.
src-tauri/src/commands/iconos.rs The five commands below.

Dependencies: @iconify/utils (runtime dependency, used by the script) and @iconify/json (dev dependency with every collection; listed in knip.json ignoreDependencies because only the script reads it from node_modules).

Bundled icons (pnpm iconos)

flowchart LR
    SRC["src/**/*.tsx<br/>&quot;game-icons:crown&quot;"] -->|pnpm iconos| GEN["src/lib/iconos/incluidos.ts"]
    JSON["@iconify/json"] --> GEN
    GEN --> ICONO["&lt;Icono&gt;"]
    ICONO -->|id not bundled| STORE["useIconos.pedir"] -->|iconos_datos| RUST["iconos.rs"]
Loading

After adding or changing an icon id in the code, run pnpm iconos. pnpm check runs pnpm check:iconos (node scripts/iconos.mjs --check), so a forgotten regeneration fails locally and in CI. Why generate instead of importing per icon: one component for both kinds of icon (written and chosen), no network for the app's own icons, and only the icons actually used end up in the bundle.

An id built with a template (`lucide:${x}`) is invisible to the script: write the full ids somewhere in the code (a table) so they get bundled. Not bundled means fetched from the network at run time, which the app's own icons must never need.

Which collection: lucide:* for general interface icons (the same family the app used before), game-icons:* for game concepts on the «Jugar» screens and the game log: poison (bottle), monarch (crown), initiative (dungeon gate), the city's blessing (castle), graveyard (tombstone), the day/night sun and moon, «doesn't untap» (padlock), phased out (portal), temporary notes (hourglass), and so on. Inferred: other collections are fine when they fit, as long as the id is written literally.

Chosen icons (the picker)

SelectorIcono is a button showing the current icon (or a placeholder) that opens a popover, modelled on the IconPicker of the project's website, but every request goes through Rust because the webview never goes online:

  • Collection chooser: every visible Iconify collection (iconos_colecciones, sorted by category and name, with how many icons each has) plus «Todas las colecciones», which searches by name across all of them (iconos_buscar). It opens on the collection of the current icon, or on game-icons when there is none.
  • Filter: inside a collection, a local filter over its names (iconos_nombres); in «all collections», the search text («Buscar en todas (2 letras o más)…»: at least two letters).
  • Paging: 240 icons at a time (PAGINA) and a «Mostrar más» button for the next batch; the visible ones are fetched together through useIconos.
  • Picking calls iconos_elegir(id), which stores the icon's data for offline use, and returns the full id (game-icons:crown) to onChange. With quitable, «Sin icono» clears it.

The collections, names and searches already loaded are also kept in module variables for the rest of the session.

Rust side (iconos.rs)

Command Does
iconos_colecciones() GET api.iconify.design/collections → Vec<ColeccionIconos { prefijo, nombre, total, categoria }>, hidden collections and odd prefixes left out.
iconos_nombres(prefijo) GET /collection?prefix=… → every name (categorised and uncategorised), sorted.
iconos_buscar(texto) GET /search?query=…&limit=999 (the API maximum); the text is lower-cased, cut to 40 characters, letters, digits, spaces and hyphens only. Results cached in memory per query.
iconos_datos(ids) BTreeMap<id, DatosIcono { body, vb }>, from memory, from the chosen icons on disk or from GET /<prefix>.json?icons=a,b,… (120 per request, at most 2,000 ids per call).
iconos_elegir(id) Like iconos_datos for one id, then writes it to the chosen-icons file. NotFound if the icon does not exist.
  • Network: through fuentes::Externo with the new allow-list entry ("api.iconify.design", "/"), so redirects are re-checked and responses size-capped (8 MiB of JSON, MAX_JSON). See External Sources.
  • Ids: partir accepts only prefix:name with lower-case ASCII letters, digits, - and _, up to 64 characters each; nothing else ever reaches a URL (los_ids_raros_no_llegan_a_la_url). id_valido is used by the library to refuse a bad tag icon.
  • Aliases: resolver follows up to 6 alias hops, inheriting size and combining rotations and flips; a_svg does what iconToSVG of @iconify/utils does (transforms in a <g>, width and height swapped for 90°/270°), so bundled and downloaded icons look the same.
  • Sanitising (seguro): the body is inserted into the page as-is (dangerouslySetInnerHTML), so any body with <script, foreignObject, javascript:, <image, <iframe, <!entity, an on…= handler, or an href/url( that is not a local #id is dropped (el_svg_peligroso_se_rechaza). Bundled icons come from @iconify/json at build time and are trusted.
  • Storage: the catalogue (the collection list and the names of each collection) is cached in app_cache_dir/iconos/ (colecciones.json, <prefix>.json) and renewed after 7 days (RENOVAR); it can be deleted. Icon data seen in the picker lives in memory only. The chosen icons, which something stored refers to, go to app_data_dir/iconos/elegidos.json (written atomically), so a tag icon still shows with no network or after clearing the cache. See Storage and Blobs.

Where chosen icons are used

Tests

  • Rust: iconos::tests (ids that could break a URL are refused, dangerous SVG is refused while url(#a) and names containing «on» are not, an alias inherits from its parent and is flipped).
  • pnpm check:iconos keeps incluidos.ts in step with the code.

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally