# Sandbox: agentes, incrustación y presets

> Las herramientas WebMCP para agentes de IA, el componente PostextSandbox dentro de tu aplicación con sus props y su CSS, y el formato de paquete de preset

- Versión HTML: https://postext.dev/es/docs/sandbox-embedding
- Última actualización: 2026-10-16
- Tiempo de lectura: 13 min
- Otros idiomas: [en](https://postext.dev/en/docs/sandbox-embedding.md), [ca](https://postext.dev/ca/docs/sandbox-embedding.md), [de](https://postext.dev/de/docs/sandbox-embedding.md), [fr](https://postext.dev/fr/docs/sandbox-embedding.md), [it](https://postext.dev/it/docs/sandbox-embedding.md), [pt](https://postext.dev/pt/docs/sandbox-embedding.md), [zh](https://postext.dev/zh/docs/sandbox-embedding.md), [ja](https://postext.dev/ja/docs/sandbox-embedding.md), [ar](https://postext.dev/ar/docs/sandbox-embedding.md)

## En pocas palabras

Esta página es para quienes crean sitios web o usan asistentes de IA. Un asistente que funciona en el navegador puede leer y cambiar el libro del Sandbox con un conjunto de herramientas. Puedes poner el Sandbox dentro de tu propia web como un solo componente, y esta página enumera sus opciones y los estilos que necesita. La última sección describe los archivos de un libro de ejemplo, que el Sandbox puede abrir.

## Agentes: WebMCP

El Sandbox entrega a un agente que trabaje en el navegador sus propias herramientas mediante [WebMCP](https://webmachinelearning.github.io/webmcp/) (`document.modelContext`), de modo que un asistente actúa sobre el libro mismo en lugar de ir pulsando por la interfaz. No hay nada que configurar: las herramientas quedan registradas mientras el Sandbox está en pantalla, y el resto de este sitio registra unas pocas propias en cada página (buscar en la documentación y en el Recetario, leer una página como Markdown, abrir una receta en el Sandbox, cambiar de idioma y de tema).

Están pensadas para el ciclo que un diseñador recorre a ojo: cambiar algo, mirar la página, volver a cambiar.

- **Ver** — `sandbox_get_state`, `sandbox_render_page`, `sandbox_inspect_pages`, `sandbox_inspect_point`, `sandbox_inspect_comic`, `sandbox_get_warnings`: el libro con sus capítulos y sus rangos de páginas, y lo que pasa en la interfaz (la selección, un aviso, una pregunta pendiente de respuesta); cualquier página como JPEG, PNG o WebP al ancho pedido, o un recorte para verla de cerca, con la vista previa de impresión si se pide; la misma página como datos (sus bloques y líneas, cuánto se llena cada columna, qué líneas quedan flojas o terminan en palabra partida); lo que hay en un punto de la página (la línea del texto fuente, un enlace y adónde lleva, la celda de una tabla); las viñetas y los bocadillos de una página de cómic; y lo que encuentra la revisión en cualquier capítulo o en el libro entero, sean cuales sean el capítulo y el visor en pantalla.
- **Cambiar** — `sandbox_set_config`, `sandbox_get_config`, `sandbox_find_setting`, `sandbox_edit_text`, `sandbox_write_text`, `sandbox_read_text`, `sandbox_search_text`, `sandbox_select_text`, `sandbox_undo_text`, `sandbox_manage_chapters`, `sandbox_save_resource`, `sandbox_save_font`, `sandbox_edit_comic`, `sandbox_import_word`: cualquier ajuste por su ruta (`bodyText.fontSize`, `page.margins.top`, `headings.levels[level=2].fontSize`, `header.elements[id=titleOdd].italic`), el texto mediante buscar y reemplazar exactos, con deshacer, los capítulos, las imágenes, los dibujos SVG, los vídeos, las tablas con su formato, las fuentes, las viñetas y los bocadillos de un cómic y un documento de Word importado con sus estilos asignados. Un cambio responde con cada valor antes y después y con las páginas que ocupa cada capítulo; un ajuste dice además en qué lo resuelve el motor y qué otros ajustes lo han seguido, y un valor que el motor no puede resolver se rechaza sin cambiar nada.
- **Mostrar** — `sandbox_set_view`, `sandbox_go_to_page`, `sandbox_run_viewer`: la pestaña del visor, el panel lateral, capítulo o libro entero, el zoom, el ajuste y las páginas enfrentadas, el tamaño de letra del lector, la vista previa de impresión, las guías dibujadas sobre las páginas, la lupa del Folio y la página en pantalla (por su número, por un pasaje, por un ancla o por una entrada del índice de contenidos de un EPUB); volver a generar el PDF o el EPUB, imprimir y reproducir un vídeo de una página.
- **Guardar** — `sandbox_list_books`, `sandbox_open_book`, `sandbox_save_book`, `sandbox_import_book`, `sandbox_export`, `sandbox_word_templates`, `sandbox_answer`: las muestras y tus proyectos con sus portadas, un archivo `.postext` importado, el libro —o uno que no está abierto— exportado a PDF, EPUB, `.postext`, Word, Markdown o sus ajustes en JSON, las plantillas de la importación de Word y la respuesta a una pregunta que hace el Sandbox.

Una herramienta que cambia el libro espera a la maquetación, así que la siguiente imagen es siempre la de las páginas nuevas; la página se maqueta y se pinta para la herramienta sea cual sea la pestaña en pantalla. Lo que no se puede deshacer —borrar un capítulo, un recurso o un proyecto, restaurar el original, importar un documento de Word sobre el texto, sustituir tu copia de un libro— va marcado como consecuente, para que el navegador pueda preguntarte antes. Igual que con la interfaz, todo se queda en tu navegador: las herramientas no alcanzan nada que el propio Sandbox no alcance.

WebMCP es un borrador: Chrome lo ofrece como prueba de origen y tras `chrome://flags/#enable-webmcp-testing`. En un navegador que no lo tenga, las mismas herramientas están en `window.postextWebMcp` (`listTools()`, `executeTool(name, args)`), para un agente que pueda ejecutar código en la página. Al incrustar el Sandbox, `webmcp={false}` las deja fuera (véase [Props](https://postext.dev/es/docs/sandbox-embedding.md#props)).

La página [WebMCP](https://postext.dev/es/docs/webmcp.md) explica qué se le puede pedir a un agente y cómo se llama cada herramienta, con ejemplos.

## Uso standalone

El Sandbox está disponible como un paquete React independiente (`postext-sandbox`) que puede embeberse en cualquier aplicación React:

```bash
npm install postext-sandbox postext
```

```tsx
import { PostextSandbox } from 'postext-sandbox';

function App() {
  return (
    <PostextSandbox
      initialMarkdown="# Hola Mundo"
      initialConfig={{ layout: { layoutType: 'double' } }}
      onMarkdownChange={(md) => console.log(md)}
      onConfigChange={(cfg) => console.log(cfg)}
    />
  );
}
```

### Props

| Prop | Tipo | Descripción |
| --- | --- | --- |
| `initialMarkdown` | `string` | Contenido markdown inicial |
| `initialConfig` | `PostextConfig` | Configuración de maquetación inicial |
| `className` | `string` | Clase CSS para el contenedor raíz |
| `labels` | `Partial<SandboxLabels>` | Sustituciones de las etiquetas de la interfaz, combinadas sobre los valores por defecto en inglés — pasa etiquetas traducidas junto con `locale` al embeber en otro idioma |
| `locale` | `string` | Idioma de la interfaz (`'en'` por defecto). Selecciona los tipos de recurso y los recursos de ejemplo localizados (un preset o un archivo en un solo idioma recibe los tipos de recurso de su propio idioma), y proporciona la configuración regional de separación silábica que usan las vistas previas cuando no hay ninguna configurada |
| `presetSources` | `{ url: string; private?: boolean }[]` | Fuentes de presets remotas, consultadas en orden tras el preset integrado. Cada `url` es una URL base que sirve un `index.json` (consulta [Formato de paquete de preset](https://postext.dev/es/docs/sandbox-embedding.md#formato-de-paquete-de-preset)); `private: true` etiqueta los presets de la fuente como privados y hace que su marca `default` pueda cargarse automáticamente. Las fuentes que no responden simplemente no aportan nada |
| `hashBundles` | `Record<string, (id: string, lang: string \| null) => string \| string[] \| null>` | Libros enlazados con una clave de fragmento propia del integrador: `{ recipe: (slug, lang) =>  0  }` hace que `#recipe=<slug>&lang=es` abra ese paquete. Devuelve la URL de un archivo `.postext` del mismo origen que la página, varias para probar en orden (gana la primera que responde) o `null` para un id que no se sirve. La primera visita importa el archivo como un proyecto que recuerda el enlace (`recipe:<slug>:<lang>`); una visita posterior pregunta si abrir ese proyecto, con sus cambios, o sustituirlo por el archivo tal como se sirve ahora. Después el fragmento pasa a ser `#project=<id>` |
| `onConfigChange` | `(config) => void` | Callback cuando cambia la configuración |
| `onMarkdownChange` | `(markdown) => void` | Callback cuando cambia el markdown |
| `webmcp` | `boolean` | Registra las [herramientas WebMCP](https://postext.dev/es/docs/sandbox-embedding.md#agentes-webmcp) del Sandbox para agentes en el navegador mientras está montado. Por defecto `true` |
| `themeToggle` | `ReactNode` | Componente personalizado para el conmutador de tema, alojado en la parte inferior de la barra de actividad |
| `languageSwitcher` | `ReactNode` | Componente personalizado para el selector de idioma, alojado en la parte inferior de la barra de actividad |
| `homeUrl` | `string` | Si se establece, renderiza un enlace con el logo de inicio en la parte superior de la barra de actividad apuntando a esta URL |
| `homeLink` | `ReactNode` | Nodo personalizado para el hueco de inicio, que sustituye al enlace con logo por defecto (tiene prioridad sobre `homeUrl`) |

### Requisitos de CSS

El paquete usa clases utilitarias de Tailwind CSS que hacen referencia a variables CSS personalizadas. La aplicación anfitriona debe proporcionar estas variables:

- `--background`, `--foreground` — Colores base
- `--surface` — Color para superficies elevadas; `--surface-2` — Filas bajo el puntero y opciones resaltadas (por defecto, `--background`)
- `--slate` — Color para texto atenuado
- `--rule` — Color de bordes/divisores; `--rule-strong` — Filete más marcado para los controles bajo el puntero (por defecto, `--slate`)
- `--brand` — Color de acento: la marca del panel activo, los anillos de foco, los interruptores y las opciones seleccionadas
- `--brand-hover` — Color de acento al pasar el puntero
- `--brand-soft` — Relleno de acento translúcido detrás de las opciones seleccionadas (por defecto, `--surface`)
- `--brand-contrast` — Botón de un interruptor activado (por defecto, `--background`)
- `--destructive` — Color de las acciones destructivas y los errores
- `--accent-blue` — Color de enlace/acento
- `--font-sans` — Familia tipográfica de la interfaz
- `--font-mono` — Familia monoespaciada (para el editor de código)
- `--font-display`, `--brand-blue`, `--brand-gilt` — Tipo de letra y colores opcionales de la marca del logo por defecto en la barra de actividad (por defecto, Georgia, `#2b4acb` y `#d8a21a`)

El sitio web asigna a `--brand` el dorado en el tema oscuro y el azul en el claro. Las versiones anteriores leían `--gilt` y `--gilt-hover`; el sandbox ya no las usa.

Los popovers, menús y tooltips se renderizan en un portal sobre `document.body`, así que define las variables en `:root` (o `body`), no solo en el elemento que envuelve el sandbox. La compilación de Tailwind de la aplicación anfitriona debe escanear el código fuente del paquete (`@source "…/postext-sandbox/src/**/*.tsx"` en Tailwind 4) para que se generen las clases utilitarias que usa.

## Formato de paquete de preset

Apéndice. Una fuente de presets es un directorio (servido por HTTP) cuya raíz contiene un `index.json` que lista directorios de preset; cada directorio contiene un manifiesto `preset.json` más los archivos de markdown, recursos y fuentes a los que se refiere. Las herramientas de `scripts/presets/` del repositorio generan paquetes exactamente con esta forma, y un archivo `.postext` exportado desde el sandbox es uno de estos directorios comprimido en zip (sin el `index.json`).

```
<raíz-de-presets>/
  index.json
  <directorio-del-preset>/
    preset.json
    chapters/              un .md por capítulo, en orden (un manifiesto versión 1 nombra un único documento)
    resources/             figuras: .svg / .png / .jpg / .jpeg / .webp / .gif (+ originales de imprenta .pdf opcionales)
    fonts/                 .otf / .ttf / .woff2, una cara por archivo
```

### `index.json`

```jsonc
{
  "version": 1,
  "presets": [
    {
      "id": "acme-journal",          // id estable, [a-z0-9-]
      "dir": "acme-journal",         // nombre del directorio relativo a index.json
      "name": "Acme Journal",
      "description": "Artículo de revista a dos columnas",   // opcional
      "locale": "es",                 // opcional
      "default": true,                // opcional, como máximo uno
      "locales": ["es", "en"],        // opcional: todos los idiomas del paquete
      "openLocale": "es",             // opcional: el idioma en que se abre si no se pide ninguno
      "thumbnail": "thumbnail.jpg",   // opcional: vista previa en el selector
      "license": "CC BY 4.0",         // opcional: se muestra como etiqueta
      "credits": "Textos e imágenes: ESO",  // opcional: una línea bajo la descripción
      "tags": ["revista", "dos-columnas"],  // opcional
      "binding": "left"               // opcional: el borde en que la estantería de la web dibuja el lomo ("right" para un libro encuadernado a la derecha)
    }
  ]
}
```

### `preset.json`

```jsonc
{
  "version": 2,
  "id": "acme-journal",
  "name": "Acme Journal",
  "description": "Artículo de revista a dos columnas",       // opcional
  "locale": "es",                                     // opcional
  "default": true,                                    // opcional
  "configVersion": 8,                                 // opcional: las reglas de configuración para las que se escribió `config`
  // Los mismos campos opcionales de escaparate que la entrada del índice:
  // "locales", "openLocale", "thumbnail", "license", "credits", "tags"

  // Los capítulos del libro, en orden — o una lista por idioma
  "chapters": [
    { "title": "Introducción", "file": "chapters/01-introduccion.md" },
    { "title": "Métodos", "file": "chapters/02-metodos.md" }
  ],
  // "chapters": { "en": [ … ], "es": [ … ] },
  // Los manifiestos versión 1 usan un único documento y se siguen aceptando:
  // "version": 1, "markdown": "article.md"  (o un mapa idioma → archivo)

  // JSON de PostextConfig opcional (página, márgenes, columnas, tipografía...).
  // NO incluyas customFonts aquí: las fuentes vienen del array `fonts` de abajo.
  "config": { },

  "resources": [
    {
      "id": "fig-1-1",                 // referenciado desde el markdown (:ref)
      "typeId": "figure",              // id del tipo de recurso (figure, table, ...)
      "kind": "svg",                   // "svg" | "bitmap" | "table"
      "file": "resources/fig-1-1.svg", // solo svg/bitmap, relativo a preset.json
      "width": 240.5,                  // opcional, pt (svg) / px (bitmap)
      "resolution": 300,               // opcional, solo bitmap: ppp a su tamaño natural de impresión
      "pdfFile": "resources/fig-1-1.pdf", // opcional, solo svg: original de imprenta vectorial (ver notas)
      "height": 180,                   // opcional
      "caption": "Texto de la leyenda",
      "altText": "Descripción accesible",             // opcional
      "safeArea": { "x": 0.18, "y": 0.25, "width": 0.5, "height": 0.6 }, // opcional, solo svg/bitmap: la parte que siempre se ve, en fracciones de la imagen
      "placement": { "position": "top", "span": "column" },  // opcional
      "note": "Fuente: ...",                          // opcional
      "table": { "model": { "rows": [[{ "content": "A", "isHeader": true }]], "headerRowCount": 1 } }
                                                      // solo kind "table"
    }
  ],

  "fonts": [
    {
      "name": "Some Family",           // nombre de familia usado por la configuración
      "variants": [
        { "weight": 400, "style": "normal", "file": "fonts/SomeFamily-Regular.otf" },
        { "weight": 400, "style": "italic", "file": "fonts/SomeFamily-Italic.otf" },
        { "weight": 700, "style": "normal", "file": "fonts/SomeFamily-Bold.otf" }
      ]
    }
  ]
}
```

Notas:

- El `id` de `preset.json` debe coincidir con la entrada de `index.json`; `description`, `locale` y `default` del manifiesto prevalecen sobre los valores del índice una vez cargado el preset, igual que los campos de escaparate `locales`, `openLocale`, `license`, `credits` y `tags`. `openLocale`, uno de los `locales`, es el idioma en que se abre el libro cuando un enlace o un clic no piden ninguno (la estantería de la portada web también enlaza con él): úsalo cuando el original no está en el idioma del lector, como hace el escaparate de 紅樓夢 con `zh-Hant`, para que su traducción inglesa no se abra a todo lector de habla inglesa. Sin él decide el idioma de la interfaz. El selector muestra como etiquetas cada idioma de `locales` (o `locale`) y la licencia, los créditos bajo la descripción, y `thumbnail` — una pequeña imagen de página relativa al directorio del preset — en lugar de la columna de la marca de verificación; un libro copiado del preset (**Hacer una copia propia**) la conserva como portada. Los presets públicos de escaparate de `apps/web/public/presets/` usan todos los campos y llevan un `CREDITS.md` con la licencia y la procedencia de cada texto e imagen.
- `configVersion` (opcional) nombra las reglas de configuración para las que se escribieron `config` y las configuraciones de `localized`. Todo lo que se escribe desde postext 1.5 lo fija en 8. Un manifiesto sin él lo escribió postext 1.4 o anterior, y sus saltos de encabezado, el tamaño de sus fórmulas, el espacio en torno a sus figuras en línea (en el texto y en los recuadros), las marcas de sus títulos, sus letras capitulares, el sitio bajo una línea de dos puntos, los cortes de sus recuadros, los cortes tras una raya y tras el guion de un compuesto, su texto en bandera, sus divisiones bajo un título y el espacio bajo sus contenedores de párrafos se leen tal como los componía la 1.4 (consulta *Saltos de encabezado guardados con postext 1.4 o anterior*, *Tamaño de las fórmulas guardado antes de postext 1.5*, *Figuras en línea guardadas antes de postext 1.5*, *Figuras en línea de los recuadros guardadas antes de postext 1.5*, *Marcas de los títulos y letras capitulares guardadas antes de postext 1.5*, *Listas tras dos puntos guardadas antes de postext 1.5*, *Cortes de los recuadros guardados antes de postext 1.5*, *Rayas y texto en bandera guardados antes de postext 1.5*, *Compuestos guardados antes de postext 1.5*, *Divisiones bajo un título guardadas antes de postext 1.5* y *Contenedores de párrafos guardados antes de postext 1.5* en [Persistencia](https://postext.dev/es/docs/sandbox-books-saving.md#persistencia)); a uno marcado con 3 a 7 (una versión preliminar de la 1.5) solo se le aplican las fijaciones de las reglas posteriores: con 3, el tamaño de las fórmulas, el hueco en línea, las cinco fijaciones de las reglas 6, las dos de las reglas 7 y las tres de las reglas 8; con 4, el hueco en línea y las de las reglas 6, 7 y 8; con 5, las de las reglas 6, 7 y 8; con 6, las de las reglas 7 y 8; con 7, solo las de las reglas 8. Fíjalo en 8 cuando escribas a mano un manifiesto para las reglas actuales.
- `view` (opcional) indica cómo abre el sandbox el paquete: `{ "canvasScope": "book" }` maqueta el libro entero en el canvas (consulta [Libros y capítulos](https://postext.dev/es/docs/sandbox-books-saving.md#libros-y-capítulos)); si falta, el canvas muestra un capítulo cada vez. Los libros exportados lo llevan cuando está activo. Un libro de más de 200 capítulos se abre de capítulo en capítulo pida lo que pida.
- `start` (opcional) indica dónde empieza el libro cuando el paquete contiene parte de uno más largo: `{ "pageIndexOffset": 57, "pageNumbering": { "startAt": 58 }, "headings": { "h1": 3 } }` lo abre en la página 58, par, en el capítulo 4. Es la `continuation` de su primer capítulo (`buildDocument`); el sandbox maqueta el libro desde ahí, lo conserva con el libro al guardarlo como proyecto y lo vuelve a escribir al exportar. Ningún panel lo edita. Una entrada de `localized` puede llevar un `start` propio para su edición.
- `localized` (opcional) contiene las variantes por idioma de un paquete bilingüe: `{ "es": { "config": { … }, "resources": [ { "id": "fig-1", "caption": "…", "note": "…", "altText": "…" } ] } }`. Las variantes del idioma del que se tomaron los capítulos se aplican sobre el `config` compartido (las claves de primer nivel se sustituyen enteras — típicamente `resourceTypes` con los prefijos de pie traducidos) y sobre `resources` (pie, nota, texto alternativo y celdas de tabla, fusionados por id); un idioma sin entrada conserva la redacción compartida. Una entrada puede llevar además su propia `view`, que se aplica sobre la compartida a esa edición: 紅樓夢 abre sus ediciones chinas en el libro completo y la inglesa de Joly, mucho más lenta de pintar entera, de capítulo en capítulo (`"en": { "view": { "canvasScope": "chapter" } }`).
- `chapters` (versión 2) es una lista de `{ title, file }` en el orden del libro, o un mapa idioma → lista; `markdown` (versión 1) es un nombre de archivo o un mapa idioma → archivo. En ambos casos el sandbox elige la entrada que coincide con el idioma pedido (etiqueta exacta, sin distinguir mayúsculas; luego el mismo idioma y la misma escritura, de modo que `zh-TW` lee `zh-Hant` antes que `zh-Hans`; luego el mismo idioma en la otra escritura, de modo que quien pide `zh-TW` a un paquete con la clave `zh` a secas recibe el chino; luego el `locale` del propio preset y, por último, la primera entrada), y solo se leen los archivos de capítulo de ese idioma. Un `title` vacío se deriva del primer título de nivel 1 del archivo. El sandbox siempre escribe la versión 2.
- `width` / `height` son opcionales para los recursos `svg` y `bitmap` — si faltan, se leen del archivo al cargar. Un mapa de bits también puede llevar `resolution` (sus ppp a su tamaño natural de impresión) y `fileResolution` (la que indica su archivo, leída al cargar cuando la configuración fija `layout.bitmapResolution: 'file'`).
- `pdfFile` (solo recursos `svg`) apunta a un PDF de una página con la misma figura en vectores — normalmente la exportación original de Illustrator o PDF de la que se derivó el SVG. La exportación a PDF incrusta esa página tal cual (fuentes, degradados y espacios de color intactos) en lugar del SVG; las vistas previas en canvas y HTML siguen usando el SVG. Se ignora en modo de una tinta, cuyo recoloreado solo opera sobre el código SVG, y si el archivo falta solo se pierde el original, nunca la figura. Editar el código del SVG en el detalle del recurso quita el original (se hizo para el código tal como estaba), de modo que la exportación a PDF sigue el SVG editado a partir de ahí; restaura el original (**Restaurar el original…** en el panel Libros) para recuperarlo. Los SVG sin original se emiten como trazados vectoriales cuando se mantienen dentro del subconjunto del backend PDF (formas básicas, trazados, grupos, `use`, recortes, rellenos y trazos sólidos, opacidad, y textos `text` / `tspan` compuestos con las fuentes que el documento puede cargar — primero las fuentes personalizadas, después las familias de Google) y se rasterizan a 600 ppp en caso contrario.
- El `text` de un SVG se renderiza con las fuentes del libro en todas partes: las vistas previas y el EPUB incrustan en el SVG las variantes que nombra su texto antes de mostrarlo (un `<img>` no ve las fuentes de la página), las familias personalizadas tal como se subieron y las de Google como los archivos de Fontsource que contienen sus caracteres, y la exportación a PDF compone los textos como texto real y seleccionable con las mismas fuentes incrustadas. Escribe la familia en `font-family` exactamente como aparece en Fuentes. Un recurso puede excluirse con `"inlineFonts": false` en su entrada del manifiesto (`svg.inlineFonts`).
- El editor de código del SVG solo permite escribir dentro de los nodos de texto (resaltados) para que una figura no se rompa por accidente; el botón de candado de su cabecera abre todo el código — coordenadas, colores, estructura — y al volver a bloquear se vuelven a detectar los nodos de texto.
- `table.model` es un `TableModel` de Postext: `rows` es un array de celdas por filas (`content` y, opcionalmente, `colSpan`, `rowSpan`, `isHeader`, `align`, `verticalAlign`), `headerRowCount` el número de filas de cabecera iniciales y `columnWidths` un array opcional de pesos relativos de columna. `table.styleId` nombra, opcionalmente, uno de los `tableStyles` de la configuración.
- Los archivos de fuente se guardan en IndexedDB con ids derivados del id del preset y la ruta del archivo, y las familias se añaden a `config.customFonts` al aplicar el preset; un `index.json` ausente o mal formado no produce ningún preset en lugar de un error.
- `<directorio-del-preset>/fingerprint.json` es opcional y, en la ruta de desarrollo, **virtual**: cuando no existe tal archivo, `GET` responde `{ "fingerprint": "<sha1 hex>", "files": <n> }` calculado a partir de los metadatos de los archivos del directorio (ruta relativa, fecha de modificación y tamaño de cada archivo hasta cuatro niveles de profundidad, omitiendo los archivos ocultos y `node_modules`), nunca a partir de su contenido, con `Cache-Control: no-store`. El sandbox lo consulta para seguir las ediciones del paquete (consulta [Panel de libros](https://postext.dev/es/docs/sandbox.md#panel-de-libros)). Un servidor estático puede servir un `fingerprint.json` real con la misma forma, o ninguno, en cuyo caso sus presets no se siguen.
