Repository navigation
State Stores
English · Español
All frontend state lives in Zustand stores. Seven global stores in src/stores/ cover the open deck, the page plan, screen state and preferences, the running long job, the app-update state machine, the card being designed with unsaved changes and the game table's view; a handful of area stores live beside their feature. The stores never compute layout or do heavy work: they hold what Rust returned, call Rust through call(), and expose actions to the components.
| Store (hook) | File | Persistence | One-line role |
|---|---|---|---|
useDeck |
src/stores/deck-store.ts |
Rust (SQLite) via deck_save, debounced autosave |
The open deck, undo/redo history, save state. |
usePlan |
src/stores/plan-store.ts |
none (derived) | The PagePlan computed by Rust for the open deck. |
useUi |
src/stores/ui-store.ts |
localStorage key proxybuilder.preferencias (preferences subset only) |
View, zoom, selection, open dialog, open tutorial, last action, list preferences. |
useTrabajo |
src/stores/trabajo-store.ts |
none | The one long job in flight (resolve a list, export a PDF, complete a design language or prepare the game table). |
useActualizacion |
src/stores/actualizacion-store.ts |
none | In-app update state machine. |
useEdicion |
src/stores/edicion-store.ts |
none | The card being designed, when it has unsaved changes, and how to save it. |
useMesa |
src/stores/mesa-store.ts |
none (the game lives in Rust) | The game table's view as Rust sends it, patched in place, plus screen-only selection, hover, inspection and log. |
usePantalla |
src/features/mesa/pantalla.ts |
none | Game table screen state: open table dialog, target mode, drag in progress, zone under the pointer. |
useTemasInstalados |
src/lib/temas-instalados.ts |
localStorage key proxybuilder.temasInstalados (mirror) |
Installed tweakcn themes; see Themes and Skins. |
useBiblioteca, usePrefsBiblioteca, useFiltrosBiblioteca
|
src/features/library/store.ts, preferencias.ts
|
Rust for data; proxybuilder.biblioteca for view prefs; filters not persisted |
Library data and view; see Deck Library. |
useMarcosOriginales, usePacksDescargables
|
src/features/disenar/marcos-originales.ts, packs-descargables.ts
|
none | Download status/progress of downloadable frame packs; see Frame Packs. |
Theme and language state are not Zustand: they are React contexts (ThemeProvider, LanguageProvider) backed by localStorage; see Themes and Skins and Internationalization.
flowchart LR
UI["Components"] -->|"aplicar / setCartas / setAjustes"| DS["useDeck (deck-store)"]
DS -->|"subscribe: version changed"| AG["iniciarAutoguardado (800 ms)"]
AG -->|"call deck_save"| R["Rust: SQLite + blobs"]
DS -->|"subscribe: cards / settings / defaultBack changed"| PS["iniciarPlan (100 ms)"]
PS -->|"call compute_page_plan"| R
R -->|"PagePlan"| PL["usePlan"]
PL --> PV["PreviewPane / footer"]
UI -->|"seleccionar, abrir, setZoom"| UIS["useUi"]
UIS -->|"localStorage proxybuilder.preferencias"| LS[("localStorage")]
UI -->|"empezar / avanzar / terminar"| TS["useTrabajo"]
R -->|"events resolve:// export://"| LT["lanzarTrabajo (lib/trabajos.ts)"]
LT --> TS
UPD["useActualizacion"] -->|"call update_check / update_download / update_install"| R
UPD -->|"guardarAhora before install"| DS
DSG["DisenarScreen"] -->|"poner({ guardar }) while there are unsaved changes"| ED["useEdicion"]
ED -->|"pendiente.guardar()"| DSG
CC["ConfirmarCierre / AppShell"] -->|"pendientes() reads trabajo, paso, edicion"| TS
CC --> UPD
CC --> ED
CC -->|"partida: useMesa.activa"| MS
R -->|"Channel MensajeMesa: Vista / Parche"| MS["useMesa (mesa-store)"]
UI -->|"hacer(accion): call mesa_accion"| R
CC -->|"guardarAhora on close"| DS
Two derived loops are started once, from useDeckInicial() (src/features/shell/use-deck-inicial.ts): iniciarAutoguardado() and iniciarPlan(). Both subscribe to useDeck outside React and return a stop function used on unmount.
Shape: { deck: Deck | null, guardado: EstadoGuardado, errorGuardado, version: number, pasado: Instantanea[], futuro: Instantanea[] } plus actions.
Deck is the generated type (src/bindings/Deck.ts): id, name, decklistText, cards: DeckCard[], settings: PrintSettings, defaultBack, schemaVersion (model details in Deck Model).
EstadoGuardado = "saved" | "saving" | "dirty" | "error" (rendered by src/features/shell/save-indicator.tsx, labels "Saved / Saving… / Unsaved / Couldn't save").
| Action | Behaviour |
|---|---|
cargar(deck) |
Replaces the deck and resets guardado: "saved", pasado/futuro: [], version: 0. |
aplicar(cambio) |
The single mutation path with history. cambio(deck) returns a Partial<Deck>; the previous Instantanea (cards, settings, defaultBack) is pushed to pasado (capped at MAX_HISTORIAL = 100), futuro is cleared, guardado = "dirty", version++. |
setCartas(f), setAjustes(f)
|
Convenience wrappers over aplicar for the card list and PrintSettings. |
setSinHistorial({ name?, decklistText? }) |
Changes that are saved but not undoable (deck name, the list text). Bumps version, marks dirty, no snapshot. |
refrescarCartas(antes, nuevas) |
For system-initiated card updates that are not user edits (re-rendered design thumbnails, src/features/disenar/renumerar.ts rehacerMiniaturas). Replaces only cards whose current object is still the same reference as in antes, so a user edit made meanwhile wins. No history; marks dirty. |
deshacer() / rehacer()
|
Pop from pasado / shift from futuro, swapping with the current snapshot; mark dirty. Do not touch name or decklistText. |
_marcarGuardado(estado, error?) |
Internal: used by the autosave and guardarAhora. |
Invariants:
- Never call
deck_savefrom a screen. All persistence goes throughiniciarAutoguardado()(orguardarAhora()right before leaving the deck). - Any change must go through
aplicar/setSinHistorial/refrescarCartassoversionincrements; both derived loops key off it. - History holds whole
cardsarray references (structural sharing), not deep copies, so 100 entries are cheap. Edits must therefore be immutable (use the pure helpers insrc/lib/deck-ops.ts:mover,moverGrupo,duplicar,quitar,insertar,actualizar,huecos).
- Subscribes to
useDeck; whenversionchanged andguardado === "dirty", (re)starts an 800 ms timer (debounce). - On fire,
guardar()reads{ deck, version }; if a save is already in flight it setspendiente = trueand returns (only onedeck_savein flight). - Sets
"saving",await call("deck_save", { deck }), then sets"saved"only ifuseDeck.getState().version === version(no change arrived during the save), else"dirty". - On failure:
"error"with the error kept inerrorGuardado(the indicator's tooltip showsmensaje(error)). The retry happens with the next change (the subscriber needsguardado === "dirty", whichaplicarsets); there is no automatic retry loop. - In
finally: ifpendienteor the state is"dirty", schedule another round with the newest version.
Related helpers in the same file:
| Function | Use |
|---|---|
abrirDeckInicial(nombreNuevo) |
Boot: reads app setting last_deck_id (app_setting_get), tries deck_open, falls back to deck_create, writes last_deck_id, then cargar. A stale id (deleted deck) silently falls back to a new deck. |
guardarAhora() |
Flushes pending changes immediately (deck_save, then "saved" if the version did not move). Used before switching deck (abrirDeck), before loading the library (useBiblioteca.cargar), before installing an update and on every close request of the window (ConfirmarCierre, so a change made inside the 800 ms autosave wait is not lost; a failure there becomes a reason to ask, see Settings and Shell). |
abrirDeck(id) |
guardarAhora() then deck_open, remember last_deck_id, cargar. |
Undo shortcuts (Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z) are registered in useDeckInicial and skip focused inputs/textareas/contenteditable so text fields keep their own undo.
Persistence side: deck_save is the Rust command in src-tauri/src/commands/library.rs returning SaveResult { updatedAt }; storage is described in Storage and Blobs.
Shape: { plan: PagePlan | null, error: unknown, deDeck: Deck | null }. deDeck is the exact deck object the plan was computed from, so consumers can map a cell index to the card that existed when Rust computed it.
iniciarPlan(retardoMs = 100):
- Subscribes to
useDeck. If the deck object is the same, nothing. If the deck id changed or it becamenull/non-null, request immediately (programar(true)). Otherwise request after 100 ms only ifcards,settingsordefaultBackchanged by reference (name and list text do not affect layout). - A monotonically increasing
peticioncounter implements "last request wins": a late response from an older request is dropped. On error onlyerroris set; the previousplanstays visible. - Rust command:
compute_page_plan({ deck })(src-tauri/src/commands/layout.rs); geometry is in Layout Engine. Nothing on the screen recomputes positions;PagePlanis in millimetres and the preview multiplies by zoom (see Live Preview).
Two groups of fields in one store.
Session-only (not persisted):
| Field | Meaning |
|---|---|
vista: "deck" | "biblioteca" | "disenar" | "mesa" / setVista
|
Which main view AppShell shows. |
disenando: string | null / disenar(id)
|
Card id being designed; disenar(id) also switches vista to "disenar". |
verReversos: boolean / setVerReversos
|
Preview shows card backs. |
tutorial: Vista | null / abrirTutorial(v)
|
The screen whose tutorial is open. abrirTutorial(v) also appends v to tutorialesVistos (once) and saves; abrirTutorial(null) closes. See Tutorials. |
seleccion: string[], ancla: string | null / seleccionar(ids, ancla?)
|
Selected card ids and the last clicked card (anchor for Shift ranges). |
dialogo: Dialogo | null / abrir(d)
|
The open global dialog, a discriminated union on tipo: editar, variante, vincular (each with id), vincularLote (ids?, soloSeleccion?), anadir (en? insert position), moverA (ids), fichas, sets, estadisticas, colecciones, recursosDiseno, disenarDeck, exportarPng. |
ultimaAccion: string | null / anunciar(texto)
|
Footer message ("PDF saved (6 pages)"). |
Preferences (type Preferencias, persisted):
| Field | Default | Meaning |
|---|---|---|
lang |
"en" |
Language used to fetch card printings (Scryfall language code: en es fr de it pt ja ko ru zhs zht, the list IDIOMAS_SCRYFALL of src/lib/idiomas.ts). This is not the interface language (that is proxybuilder.lang, see Internationalization). Sent as ResolveOptions.lang and used by the add-card dialog. |
imagePolicy |
"withImage" |
ImagePolicy = bestAvailable / withImage / highresOnly: which printing is chosen when the list gives no set. |
addTokens |
false |
Add related tokens when resolving a list. |
calidadVista |
488 |
Preview thumbnail width (AnchoMiniatura; the selector offers 244 / 488 / 672 / 745). Screen only; the PDF always uses export resolution. |
pedirConfirmacionAlCargar |
true |
Ask before replacing existing cards when loading a list. |
zoom |
0.8 |
Preview zoom, clamped to [ZOOM_MIN 0.4, ZOOM_MAX 2.5] and rounded to 0.01 in setZoom. |
infoCarta |
false |
Show per-card info badges in the preview. |
actualizarSola |
true |
Auto-check/download updates (see Settings and Shell). |
tutorialesVistos |
[] |
Screens (Vista) whose tutorial has already been shown; useTutorialInicial only opens a tour by itself for a screen not in the list. See Tutorials. |
mesa |
MESA_DE_FABRICA |
PreferenciasMesa, the game table settings (card size, quality, large view, status panel, animations, shadows, frame counter, sounds of the cards, the turn and the chat, hold-to-open menu, bonus offer, phantom, trackers, live movement online, following the turn in a local game and the layout of the boards per number of players). Changed with setMesa(partial); read back merged over MESA_DE_FABRICA so a new field gets its default. See Game Table. |
Persistence: leer() merges the stored JSON over PREDETERMINADAS, so adding a new preference never breaks older stored objects; guardar() writes only the Preferencias keys (never vista, selection or dialog) and is called by setZoom, setInfoCarta, setPreferencia(k, v), setMesa and abrirTutorial. Both are wrapped in try/catch (private mode, blocked storage): the app then simply runs on defaults for the session. setVerReversos is deliberately not persisted.
Guard: src/stores/__tests__/ui-store.test.ts (defaults, merge over stored, broken JSON, blocked storage, zoom clamp, the exact set of persisted keys, a tutorial marked as seen once and remembered while the open one is not, and the table settings merged with their defaults).
actual: EstadoTrabajo | null where EstadoTrabajo = { tipo: TipoTrabajo, jobId: number | null, fase: string, hechas, total }. Actions: empezar(tipo, fase), setId(jobId), avanzar(fase, hechas, total), terminar(). Only one job at a time: resolver() in src/features/decklist/acciones.ts throws { code: "Busy" } if actual is set; fase is already translated text. The left panel footer and the export button read it; TipoTrabajo is "resolver" | "exportar" | "idioma" | "mesa"; "mesa" is preparing a game (mesa_empezar, mesa_retomar, see Game Table); "idioma" is the design_complete_lang job started by completarIdioma (src/features/disenar/renumerar.ts) when the deck switches to a design language its designs lack, or before a PNG export in such a language (see Designer UI). cancelarTrabajo() (src/features/decklist/acciones.ts) calls export_cancel for "exportar" and job_cancel otherwise, with jobId. The jobId is null until lanzarTrabajo returns (see Frontend Architecture). On the Rust side the matching guard is AppState::ocupado(), which also makes update_install return Busy so an update never kills a PDF export; it is also true while a game is open on the table.
Other long operations (identify batch, design PNG export, original-frames download) keep their own local progress state instead of useTrabajo.
State: soportada: boolean | null, actual (running version), paso, info: ActualizacionInfo | null, progreso: number | null (0 to 100), error.
stateDiagram-v2
[*] --> inactiva
inactiva --> buscando: buscar()
buscando --> alDia: no newer version
buscando --> disponible: update_check found one
buscando --> inactiva: error (no info)
disponible --> bajando: bajar()
bajando --> lista: update_download ok
bajando --> disponible: download error
lista --> instalando: instalar()
instalando --> lista: install error (can retry)
instalando --> [*]: app restarts
(alDia is the code value "al-dia".)
| Action | Behaviour |
|---|---|
iniciar() |
Asks Rust once (update_status) whether this build can update (soportada is false in dev or when the binary did not come from an installer); errors mean unsupported. |
buscar() |
Ignored while buscando/bajando/instalando. If already lista returns the downloaded info (no re-download until restart). Calls update_check. |
bajar() |
Only from disponible. Subscribes to update://progress ({ hecho, total }, total 0 = unknown size) to compute progreso, calls update_download({ version }), always unsubscribes. |
instalar() |
Only from lista. with the table open (useMesa.activa) it only sets alSalirDeLaMesa (the install runs when the table closes, from useAvisosActualizacion); otherwise guardarAhora() first (flush the deck), then update_install (a Busy motivo: partida also sets alSalirDeLaMesa); if it succeeds Rust restarts the app and the promise never settles; on error returns to lista and rethrows so the caller toasts it. |
The Rust side (src-tauri/src/actualizar.rs, src-tauri/src/commands/actualizar.rs) and the release pipeline are documented in Releases and Auto Updates; the UI consumers (toasts, bar button, dot, Settings section) are in Settings and Shell.
State: pendiente: { guardar: () => Promise<boolean> } | null and the action poner(p). It is a tiny registration slot, not a copy of the draft: DisenarScreen (the Design view) puts a guardar callback in it while its draft has unsaved changes and clears it when they are saved, discarded or the screen unmounts (see Designer UI). guardar returns false when saving failed; the failure was already shown to the user, so callers just do not continue.
Why a store: the things that can interrupt Design (changing view in AppShell, closing the window in ConfirmarCierre) live elsewhere and must be able to say "Save and continue" without knowing how a card is saved (design_save, applying to the other copies, announcing "Diseño guardado"). pendiente !== null is also the signal that there is something to lose: lib/cierre.ts reads it as the edicion reason. No persistence, no history.
The view Rust sends (mesa::vista::VistaMesa), normalised by key: cartas (Record<IdCarta, VistaCarta>), zonas (by claveZona(RefZona): "<player>:<zone>" or "c:<zone>"), jugadores, asientos (seat order), general, plus version, yo, formato, variantes, opciones. Screen-only fields that never travel to Rust: activa (a game is open and the screen is subscribed), registro (the last MAX_REGISTRO = 500 events, for the log), seleccion, sobre (the card under the pointer, for the large view and shortcuts) and inspeccion.
| Action | Behaviour |
|---|---|
recibirVista(v, registro?) |
Replaces everything with desdeVista(v); keeps only the selection and the hovered card that still exist. With registro (a full view from the channel: subscribing, undo, restart) it also replaces the log with the events that still count; without it (mesa_vista after a gap) the log stays. |
recibirParche(p, evento) |
conParche(m, p): only if p.version === version + 1; copies just the touched maps and leaves untouched cards as the same objects (their memoised components do not repaint). Returns false on a gap, and conexion.ts then asks for mesa_vista. Appends the event to registro and drops removed cards from selection and hover. |
cerrar() |
Back to MESA_VACIA, activa: false. |
seleccionar, alternar, setSobre, inspeccionar
|
Screen state. |
Rules: it never recomputes what Rust decides (who sees what, positions); selectors that may return nothing return the shared NINGUNA array (a fresh [] would look like a change on every read), and selectors that build a list use useShallow. Helpers cartasDe(m, zona) and cuantasEn(m, zona) read a zone. The subscription itself lives in src/features/mesa/conexion.ts. Guard: src/stores/__tests__/mesa-store.test.ts, fed by a fixture the engine writes (src/stores/__tests__/fixtures/mesa-parches.json). See Game Table.
-
useBiblioteca(src/features/library/store.ts):decks,detalles(per-deck computed details),etiquetas; every action calls Rust (deck_tags_set,deck_pin,deck_lang_set,deck_notes_set,deck_cover_set, tag CRUD) and then re-reads (refrescar).cargar()begins withguardarAhora()so the library never shows stale data for the open deck. Details in Deck Library. -
usePrefsBiblioteca: view (cuadricula/lista), sort, direction, grouping, card size, sidebar, favourites-first; persisted underproxybuilder.biblioteca.useFiltrosBiblioteca: filters, intentionally not remembered. -
usePantalla(src/features/mesa/pantalla.ts): the table's open dialog (DialogoMesa, one at a time), the target mode (adjuntarorflecha), the drag in progress (Arrastre, which the dragged copy follows without waiting for Rust; the large view hides while it is set), the zone under the pointer (so library shortcuts work) andtapeteCambiado, a counteravisarTapete()raises when the mat dialog saves, so the board reads the mat again.pedirNumeroopens the number prompt. -
useChat(src/stores/chat-store.ts): the table's chat on this screen only —silenciados(players muted for me; the event still travels),bocadillos(the last message or notice of each player, forBOCADILLO_MS),sinLeer(unread counter of the tab) andlimpiar()when the table closes.useRepeticion(src/stores/repeticion-store.ts): the replay in progress (activa:VistaRepeticionornull) and its playback (reproduciendo,velocidad); while it is set the table is read-only.usePantallaalso keeps the sidepanel(registro,chator none) andANCHO_PANEL. -
useMarcosOriginales/usePacksDescargables:info/lista,progreso, and aversioncounter that increments after a successful download so lists of packs re-fetch.
| Where | Key | Owner |
|---|---|---|
Rust app_settings
|
last_deck_id |
deck-store.ts (abrirDeckInicial, abrirDeck) |
Rust app_settings
|
carpeta_importar, carpeta_exportar
|
commands/carpetas.rs via lib/dialogos.ts
|
localStorage |
proxybuilder.preferencias |
useUi |
localStorage |
proxybuilder.lang |
LanguageProvider |
localStorage |
proxybuilder.theme, .mode, .scale, .skin, .bg, .material, .blur, .noise, .shadow, .fxSidebar, .heraFiligree, .smoothScroll
|
ThemeProvider |
localStorage |
proxybuilder.temasInstalados |
lib/temas-instalados.ts |
localStorage |
proxybuilder.biblioteca |
usePrefsBiblioteca |
localStorage |
proxybuilder.paneles, proxybuilder.panel-lista, proxybuilder.paneles.disenar
|
react-resizable-panels layouts |
localStorage |
proxybuilder.seccion.<id> |
Seccion collapsed state |
localStorage |
proxybuilder.disenar.coleccion |
last collection used for new design drafts (features/disenar/use-borrador.ts) |
localStorage |
proxybuilder.mesa.nombre |
the player name of the game table (features/mesa/entrada.tsx) |
Rust app_settings
|
mesa.tapete.<deck id>, mesa.tapete
|
the mats of the game table (src-tauri/src/mesa/tapete.rs) |
Rule of thumb: anything a deck depends on goes in the deck (Rust); anything that is a per-machine convenience goes in localStorage wrapped in try/catch; anything Rust needs to remember across sessions (last deck, last folders) goes in app_settings.
src/stores/__tests__/: deck-store.test.ts (five rapid changes cause one save; undo three times and redo restore exact state; a failed save leaves error and retries on the next change; non-history changes are saved but not undoable), deck-store-abrir.test.ts, plan-store.test.ts (immediate first request, debounce, last-request-wins with mocked call), ui-store.test.ts, trabajo-store.test.ts, actualizacion-store.test.ts, mesa-store.test.ts (patches over the first view give the final view, a version gap asks for the view, untouched cards keep their identity, the selection loses removed cards, a view with a log replaces it and one without keeps it). The close guard that reads useTrabajo, useActualizacion, useEdicion and useMesa is tested in src/lib/__tests__/cierre.test.ts (see Testing and CI). They use fake timers and mock @/lib/ipc; localStorage is stubbed because Vitest runs in the node environment.
- New undoable deck change: call
useDeck.getState().setCartas(...)/setAjustes(...)with a pure helper; do not mutate. If it is not a user edit, userefrescarCartas. - New preference: add the key to
PreferenciasandPREDETERMINADAS, and to the object built inguardar()ofui-store.ts; read withuseUi((s) => s.key). A game table setting goes inPreferenciasMesaandMESA_DE_FABRICAinstead. - New global dialog: add a
Dialogovariant, mount the component insrc/App.tsx. - New long job: use
lanzarTrabajoand mirror progress intouseTrabajoif the footer/export button should show it.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia