Repository navigation
Convenciones
English · Español
Las reglas que sigue el código, cada una con su motivo, y cómo viaja un cambio típico desde Rust hasta la pantalla. La mayoría de las reglas las hace cumplir una puerta (Biome, clippy, scripts/*.mjs, tests); el resto son convenciones de revisión marcadas en los comentarios del código con el emoji de regla dura.
Cuando una regla la hace cumplir una herramienta, se nombra la herramienta para que veas exactamente qué rechazará. Véase Tests y CI para las puertas y Preparación del entorno de desarrollo para saber cómo ejecutarlas.
| Regla | Motivo | La hace cumplir |
|---|---|---|
| Todo lo pesado vive en Rust: análisis de la lista, Scryfall, descargas, caché, geometría de página, procesado de imagen, PDF, estadísticas. React solo pinta y guarda el estado de pantalla. | Una sola implementación, velocidad nativa en todos los núcleos, salida determinista y un webview que nunca toca la red. | Revisión; la CSP de src-tauri/tauri.conf.json solo permite connect-src a ipc:; el fichero de capacidades no tiene permiso fs
|
Una sola geometría para pantalla y papel. PagePlan (milímetros) lo calcula proxy-core::layout y lo consumen tanto la vista previa como el PDF. La pantalla nunca recalcula posiciones. |
Lo que ves tiene que ser lo que se imprime. Una segunda geometría en TypeScript acabaría desviándose. | Revisión; el plan lo trae compute_page_plan a src/stores/plan-store.ts
|
Las imágenes de los mazos viven en el almacén de blobs (app_data_dir/blobs/, direccionadas por SHA-256), no en la caché. La caché (app_cache_dir/renders/) solo guarda derivados y se puede borrar en cualquier momento. |
Los mazos dependen de sus imágenes; vaciar la caché nunca debe romper un mazo. |
BlobStore::gc (src-tauri/src/store/blobs.rs); test vaciar_la_cache_no_toca_las_imagenes_de_los_decks en src-tauri/src/commands/library.rs
|
La vista previa desmonta las imágenes de las páginas que no se ven (IntersectionObserver cuyo root es el viewport del ScrollArea compartido). |
Los mazos de cientos de cartas, si no, mantendrían cientos de imágenes decodificadas. | Revisión; véase Vista previa en vivo |
proxy-core es puro: sin Tauri, sin red, sin base de datos; tiene sus propios tipos de error y la app los convierte a AppError. |
Tests rápidos (cargo test -p proxy-core en segundos), reutilización y una frontera dura contra los efectos secundarios. |
src-tauri/crates/proxy-core/Cargo.toml simplemente no tiene esas dependencias |
src-tauri/src/lib.rs solo arranca y registra comandos; cada área tiene su src-tauri/src/commands/<área>.rs. No debe crecer. |
El lib.rs de un proyecto hermano anterior llegó a unas 12 000 líneas. |
Revisión |
Los errores son códigos, nunca prosa. Los comandos devuelven Result<T, AppError> ({ code, params }); la pantalla traduce con useLang().mensaje(e). Rust nunca escribe texto para el usuario. |
La interfaz puede estar en español o en inglés; un único sitio (src/lib/errores.ts) es dueño de la redacción. |
Sistema de tipos: AppError/ErrorCode en src-tauri/src/error.rs; véase IPC y manejo de errores
|
Los tipos compartidos vienen de Rust: #[derive(TS)] #[ts(export)], y luego pnpm bindings escribe src/bindings/. Nunca se editan esos ficheros a mano. |
Una única fuente de verdad para el formato de transmisión. |
.cargo/config.toml (TS_RS_EXPORT_DIR); Biome y Knip ignoran la carpeta |
La pantalla habla con Rust solo a través de src/lib/ipc.ts (call() y on()), nunca con un invoke suelto. |
La interfaz Commands hace que el compilador compruebe los nombres y tipos de los comandos. |
Revisión; solo ipc.ts importa invoke
|
La vista web nunca sale a internet. Toda la red es de Rust. Todas las descargas externas pasan por fuentes::Externo (lista cerrada de host+ruta, redirecciones vueltas a comprobar, tope de tamaño, imágenes siempre decodificadas y recodificadas). |
Seguridad y privacidad; la CSP lo prohíbe de todos modos. Decodificar y recodificar elimina cualquier cosa hostil de los ficheros de terceros. | CSP; src-tauri/src/fuentes/ (véase Fuentes externas) |
Los diálogos de ficheros solo a través de src/lib/dialogos.ts (abrirParaImportar, guardarParaExportar). |
Recuerdan por separado las últimas carpetas de importar y de exportar (guardadas en Rust: commands/carpetas.rs, app_settings). |
Revisión |
| Los trabajos largos siguen un único patrón (abajo). | La interfaz puede mostrar el progreso, cancelar y bloquear acciones que entren en conflicto. |
src-tauri/src/jobs.rs, src/lib/trabajos.ts
|
El encuadre de la imagen estira al hueco exacto (imaging::frame) y recorta solo si la proporción difiere en más de un 15 % (MAX_ESTIRADO). |
Es preferible una ligera deformación a perder ilustración o dejar huecos. | Tests en imaging/frame.rs
|
Un comando que tarda más de un instante devuelve de inmediato un job_id numérico y trabaja en spawn_blocking:
-
let (job_id, cancelado) = state.jobs.alta();(una bandera de cancelación,jobs.rs). - Dentro de la tarea,
let _marca = estado.trabajo();marca la app como ocupada mientras viva la guarda (AppState::ocupado()). - El progreso sale como eventos
area://progressy el final comoarea://done, ambos conjob_id;job_cancel/export_cancelvoltea la bandera. - El frontend usa
lanzarTrabajo(progreso, final, lanzar, alProgreso)desrc/lib/trabajos.ts, que se suscribe antes de lanzar (un trabajo corto puede terminar antes de que elinvokedevuelva su id; los eventos tempranos se almacenan en un búfer por id).
Estar ocupado bloquea update_install y blob_gc con ErrorCode::Busy. Ejemplo: export_pdf en src-tauri/src/commands/export.rs.
El sistema de temas, las skins (Hera, Zeus), los materiales, src/themes.css (tweakcn), src/lib/themes.ts y src/components/theme-provider.tsx se han traído sin cambios, así que sus convenciones se aplican:
| Regla | Motivo |
|---|---|
Radio 14 px, densidad 10 px (p-2.5, gap-2.5), texto base text-xs
|
Coherencia visual; la app es densa por diseño |
El scroll siempre usa el ScrollArea compartido (src/components/ui/scroll-area.tsx), nunca overflow-auto en código nuevo |
Barras de scroll coherentes y comportamiento de scroll virtual en WebKitGTK; el root del IntersectionObserver es su viewport |
Las listas cuyos items tienen borde o anillo llevan padding p-1
|
Para que el contenedor de scroll no corte los anillos |
Nada de title= nativo; usa <Tooltip> o WithTooltip (src/components/ui/with-tooltip.tsx) |
GTK dibuja los tooltips nativos con los colores del sistema. Lo hace cumplir pnpm check:title (scripts/sin-title-nativo.mjs) |
WithTooltip debe ser el envoltorio exterior de otro *Trigger asChild de Radix |
Si no, se rompe la fusión de refs y manejadores de Slot y el desplegable o popover deja de abrirse (documentado al principio de with-tooltip.tsx) |
| Prueba el scroll y las animaciones en WebKitGTK, no en Chrome | WebKitGTK es el webview real de Linux y se comporta de otra manera |
El texto no se puede seleccionar (regla en src/index.css); solo los campos y los cuadros de copiar llevan select-text
|
Es una app de escritorio, no un documento; WebKit se niega a seleccionar dentro de un input si algún ancestro tiene user-select: none, así que se restaura de forma explícita |
Los modales son 100 % opacos con cualquier material o skin (regla al final de src/index.css) |
Con materiales de cristal o mate y la skin Hera, un diálogo dejaba ver el contenido de detrás y costaba leerlo; la regla del final de src/index.css fuerza el color de la tarjeta y quita el desenfoque de fondo de [data-slot="dialog-content"]
|
theme-provider.tsx es de solo lectura; solo acepta temas presentes en THEMES; los temas de tweakcn instalados se añaden al arrancar con src/lib/temas-instalados.ts
|
Mantiene intacto el fichero heredado para poder resincronizarlo |
Un diálogo con un proceso largo o un estado sin guardar propio lo declara con useEstadoParaCerrar({ enCurso, sinGuardar }) (src/lib/dialogos-abiertos.ts), con los mismos valores que pasa a DialogContent como ocupado / cambiosSinGuardar
|
Cerrar toda la app pregunta por esos diálogos (Ajustes y shell). Es un hook por diálogo porque DialogContent no puede informarlo: los ficheros de src/components/ui/ son de solo lectura (heredados), así que no se editan |
La ventana arranca oculta con fondo oscuro; se muestra cuando la página ha terminado de cargar (on_page_load en src-tauri/src/lib.rs) y main.tsx también llama a show() sobre la ventana |
Evita un destello blanco |
Véanse Temas y skins y Arquitectura del frontend.
-
Todo el texto de la interfaz pasa por
t()con la cadena fuente en español como clave; cada traducción va ensrc/i18n/<código>/<área>.jsonde cada idioma de la interfaz (el inglés es la referencia; los términos siguensrc/i18n/GLOSARIO.md).pnpm idiomasfalla si falta alguna, si una clave está duplicada entre ficheros de área o si no cuadran los marcadores o las formas de plural. Las cuentas que se pasan at()son números (sintoLocaleString/toFixed):t()las formatea y elige la forma del plural. - Los textos que viven en tablas de datos y se traducen en otro sitio se envuelven en
paraTraducir("...")(función identidad) para que el comprobador los vea (por ejemplo, los textos de error desrc/lib/errores.ts). - Las variables usan marcadores
{name}:t("Descargando la {version}…", { version }). - El idioma de fábrica es el inglés (clave de
localStorageproxybuilder.lang);t()recurre a la clave en español cuando no existe traducción, por eso importa la puerta. - El README está en inglés con una copia en español,
README.es.md: cualquier cambio va a los dos, con las mismas secciones y capturas (capturas inglesas endocs/presentacion/en/, españolas endocs/presentacion/).
Detalles: Internacionalización.
| Aspecto | Convención |
|---|---|
| Sangría |
Tabuladores en todo el TS/JS/JSON/CSS (Biome, .editorconfig); cuatro espacios en Rust (rustfmt). Motivo: los tabuladores son un byte por nivel; rustfmt reformatea ficheros enteros con su propio valor por defecto, así que pelearse con él haría que el editor y cargo fmt --check se contradijeran |
| Formato de Rust en los hooks |
rustfmt --edition 2021 sobre los ficheros en staged; nunca cargo fmt en lint-staged (véase Preparación del entorno de desarrollo) |
| Lints | Clippy con -D warnings. Nada de unwrap()/expect() fuera de tests (#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] en los dos crates de Rust). Usa ?, ok_or_else o AppError
|
| Permitir un lint | Solo localmente con #[allow(clippy::...)] y un comentario que diga por qué; nunca --no-verify
|
| Idioma del código | Los comentarios, los comentarios de documentación y los mensajes de commit van en español. Los tipos de transmisión/dominio que cruzan la frontera usan nombres de campo en inglés en camelCase (#[serde(rename_all = "camelCase")]), p. ej. DeckCard, PagePlan, PrintSettings; las áreas más nuevas usan identificadores en español (actualizar, fuentes, diseno, biblioteca, Actualizador, ActualizacionInfo). Sigue al módulo que te rodea |
| Marcador de regla dura | Los comentarios que enuncian una restricción innegociable empiezan con el emoji de señal de stop; búscalo con grep para encontrar las invariantes de un fichero |
| Nombres de comandos |
snake_case, <área>_<acción>: deck_save, card_search, update_check, design_save
|
| Nombres de eventos |
<área>://progress y <área>://done: export://progress, identify://done, update://progress
|
| Stores | Zustand, uno por responsabilidad en src/stores/; selectores por campo (useUi((s) => s.actualizarSola)) |
| Carpetas de features |
src/features/<área>/, con la lógica pura separada de los componentes en ficheros .ts para que Vitest pueda probarla sin renderizar |
| Alias de rutas del frontend |
@/ es src/
|
| Código sin usar | Knip falla ante ficheros/dependencias sin usar; src/bindings/ está exento |
| Valores por defecto que recordar | Valores de impresión de fábrica: 2 mm de separación, sin sangrado, guías de corte apagadas, «rellenar las esquinas» apagado (las esquinas transparentes se imprimen del color del papel; corners::fill solo se ejecuta si se activa) |
-
Sin OpenSSL:
reqwestusarustlscondefault-features = false; CI falla sicargo tree -i opensslencuentra algo. -
pnpm 12, no npm, con
minimumReleaseAge: 1440yallowBuildsenpnpm-workspace.yaml; regenera el lockfile con la versión depackageManager. -
rusqlitese compila con SQLitebundledpara que las tres plataformas se comporten igual.zipusa solo deflate (Rust puro). El OCR usaocrs(Rust puro); los modelos se descargan en el primer uso, no se incluyen. - El contenido que no debe ir en los instaladores (marcos «Original» de Card Conjurer, packs descargables como Balatro, letras libres, letras de los temas de tweakcn) se descarga bajo demanda con un commit fijado y un SHA-256 por fichero y se guarda bajo
app_data_dir. Véase Packs de marcos.
Añadir una función que necesita datos de Rust, de abajo arriba:
flowchart TD
A["1. proxy-core: lógica pura + tests unitarios"] --> B["2. crate de la app: comando en commands/area.rs"]
B --> C["3. registrarlo en generate_handler de lib.rs"]
C --> D["4. derive TS en los tipos nuevos, pnpm bindings"]
D --> E["5. entrada en Commands de src/lib/ipc.ts"]
E --> F["6. acción del store (src/stores) o ayudante puro + vitest"]
F --> G["7. componente en src/features/area, texto vía t()"]
G --> H["8. traducciones en src/i18n/codigo/area.json"]
H --> I["9. pnpm check + puertas de Rust"]
-
Primero, la lógica pura. Si es determinista (análisis, geometría, estadísticas, imagen), ponla en
src-tauri/crates/proxy-core/src/<módulo>con tests#[cfg(test)](un dorado si es visual; véase Tests y CI). Dale su propio tipo de error. -
El comando. En
src-tauri/src/commands/<área>.rs:#[tauri::command], recibeState<'_, AppState>(yAppHandlepara los eventos), devuelveAppResult<T>. Convierte los errores del núcleo enAppErrorcon un código y parámetros.with("detalle", ...). Para trabajo largo usa el patrón de trabajos de arriba. -
Regístralo en la lista
tauri::generate_handler![...]desrc-tauri/src/lib.rs. Olvidarlo compila, pero falla en tiempo de ejecución con un comando desconocido. -
Tipos: añade
#[derive(Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)]a los tipos nuevos de petición/respuesta (usa#[ts(type = "number")]para los camposu64, como haceActualizacionProgressEvent), y ejecutapnpm bindings. -
src/lib/ipc.ts: importa el tipo generado y añade el comando a la interfazCommands(args,result), y cualquier evento aEvents. -
Estado: una acción de un store de Zustand que haga
call()al comando, o un ayudante puro junto a la feature (que se pueda probar con tests). Los errores se guardan comoAppErroren bruto y se traducen en el borde conmensaje(e). Simula@/lib/ipcen el test del store, como hacesrc/stores/__tests__/actualizacion-store.test.ts. -
El componente en
src/features/<área>/: primitivas compartidas deui/, densidad y tamaño de texto de las reglas de diseño,ScrollAreapara el scroll,WithTooltippara las ayudas,t("texto en español")para cada cadena. -
Traducciones en el
src/i18n/<código>/<área>.jsoncorrespondiente de cada idioma de la interfaz (primero el inglés, que es la referencia). -
Puertas:
pnpm check,pnpm check:rust-fmt,pnpm check:clippy,pnpm test:rust.
Ejemplo trabajado: el actualizador. src-tauri/src/actualizar.rs (lógica y envoltorio del plugin), luego src-tauri/src/commands/actualizar.rs (cuatro comandos, evento de progreso update://progress, guarda Busy), luego el registro en lib.rs, luego las entradas de src/lib/ipc.ts, luego src/stores/actualizacion-store.ts, luego features/shell/actualizacion-automatica.tsx y features/settings/actualizaciones.tsx, luego las cadenas en src/i18n/en/. Véase Versiones y actualizaciones automáticas.
- Los cambios de esquema van en una nueva migración de SQLite numerada (en el código aparecen nombres como
0003,0004); algunas columnas por mazo (fijado, notas, portada, idioma a mano) no tocanupdated_ata propósito, para que las ediciones cosméticas no reordenen el orden por «modificado». Véase Almacenamiento y blobs. - Los campos nuevos en los tipos de mazo guardados son opcionales (se añaden como campos opcionales o claves opcionales, no como variantes nuevas de enum) para que las versiones antiguas de la app sigan abriendo mazos más nuevos. Ejemplo:
CardSource::Custom { scryfallId?, externa? }. Véanse Modelo del mazo y Formato del paquete de mazo. - Las preferencias del usuario viven en
ui-store(persistido de forma selectiva) o enapp_settingsde Rust (app_setting_get/app_setting_set); los anchos de los paneles usan el almacenamiento dereact-resizable-panels.
- El trabajo pesado está en Rust; el frontend solo hace
call(). - Nada de
unwrap/expectfuera de tests; los errores son códigosAppError. - Tipos de Rust nuevos exportados,
pnpm bindingsejecutado de nuevo,ipc.tsactualizado. - Cada cadena en
t(), entrada añadida en todos los idiomas, sin claves duplicadas entre áreas,pnpm idiomaslimpio. - Sin
title=, sinoverflow-auto, sininvokesuelto, las listas con anillos llevanp-1. - Todo lo que bloquee por un trabajo largo comprueba
ocupado()/ gestionaBusy. - Un diálogo nuevo con un proceso largo o estado sin guardar llama a
useEstadoParaCerrar; una API nueva de ventana de Tauri usada desde el WebView lleva su permiso encapabilities/default.json. - El uso de red pasa por
Scryfallofuentes::Externo. - Los dos README actualizados si cambió el comportamiento de cara al usuario.
- Dejado sin commit y sin push cuando se trabaja como colaborador automático.
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