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) |
Iconos con <Icono icon="prefijo:nombre"> (src/components/icono.tsx), nunca lucide-react fuera de src/components/ui/; tras añadir un id, pnpm iconos
|
Un solo componente para los iconos de Iconify propios de la app (incluidos, sin red) y los que elige la persona; pnpm check:iconos falla si src/lib/iconos/incluidos.ts no está al día. Véase Iconos
|
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 |
Un botón desactivado dice por qué en su tooltip (WithTooltip alrededor de un span, porque un botón desactivado no recibe eventos del puntero): «Elige un deck.», «El deck está vacío.»… |
Un botón gris sin motivo parece roto |
Las acciones destructivas o irreversibles confirman con Confirmar (destructiva): borrar un preset o el índice de nombres, expulsar a alguien de una partida, dejar el servicio de amigos. Los cambios fáciles de deshacer no preguntan, ofrecen «Deshacer» en el aviso (quitar cartas, «Auto ordenar») |
Un clic perdido no debe costar trabajo, pero las ediciones de siempre no deben frenarse con diálogos |
Las acciones lentas enseñan que están ocupadas: el botón se desactiva con el icono girando mientras dura (useOcupado en src/lib/use-ocupado.ts, o estado local), y una carga que puede fallar enseña su error con «Reintentar» en su sitio (EstadoCarga) |
Sin envíos dobles, y un fallo nunca es un callejón sin salida que obliga a cerrar y abrir el diálogo |
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.
Una sola interfaz que cambia de disposición por formato; nunca hay «pantallas de móvil» aparte y el escritorio no cambia. La referencia es Móvil; estas son las reglas que debe cumplir un cambio:
-
El formato sale de
useFormato()(src/lib/formato.ts), y «¿es Android o iOS?» desrc/lib/plataforma.ts. Nunca se adivina por el user agent. -
Nada funciona solo con el ratón. Todo lo de pasar el ratón por encima o el clic derecho necesita su camino con el dedo (pulsación larga, un toque o un botón «⋯») y lo dice en un comentario
// táctil: …;pnpm check:tactilfalla si no. Un gesto por elemento: un elemento se arrastra o abre un menú con la pulsación larga, nunca las dos cosas. -
Los objetivos táctiles miden al menos 2,5 rem (unos 43 px con el texto táctil al 108 %) y la disposición respeta las zonas seguras y el teclado (
dvh,visualViewport). -
Los diálogos son
DialogoAdaptable(a pantalla completa en móvil), los paneles sonHoja, y un diálogo con secciones usaListaSecciones. El botón «atrás» de Android cierra primero la capa de Radix de arriba; un diálogo con secciones vuelve cononEscapeKeyDown. -
Los tooltips con información de verdad usan
ConAyudaoAyudaAlTocar; un botón con solo un icono lleva siemprearia-label. -
Los ficheros que elige la persona pasan por
src-tauri/src/fichero.rs(en Android son URIscontent://); lo que solo hace Android se llama desde Rust porsrc-tauri/src/android.rs. -
El TLS se configura solo en
src-tauri/src/tls.rs. - Los cambios visuales se comprueban en el emulador, en tamaño de móvil y de tableta (Preparación del entorno de desarrollo).
-
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, sinlucide-reactfuera decomponents/ui/(pnpm iconostras un id de icono nuevo), 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
- Rename-to-Tapete · Cambio de nombre a Tapete
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
- The-Card-Stamp · El sello de las cartas
- 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
- Game-Engine · Motor de la mesa
- Game-Table · Mesa de juego
- Online-Play · Juego en línea
- Friends-and-Messages · Amigos y mensajes
Mobile · Móvil
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Icons · Iconos
- 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
- Card-Search · Búsqueda de cartas
- Set-Browser · Explorador de sets
Reference · Referencia