Skip to content

Designer Composition

Pablo Martínez López edited this page Oct 25, 2026 · 7 revisions

Designer Composition

English · Español

Composition is the layer of proxy-core::diseno that decides what kind of card a design is and how to build it from the single-face painter: maquetacion classifies the card, componer assembles multi-piece cards (split, aftermath, flip, battle, meld, big cards), estructura parses rules text into abilities/chapters/levels, and cuerpos lays out and paints the resulting sections with badges.

It sits on top of the face painter described in Designer-Engine and relies on pack layouts described in Frame-Packs. Concepts are in Designer-Overview.

Two decisions per card

For every design the engine makes two independent decisions, both in src-tauri/crates/proxy-core/src/diseno/maquetacion.rs:

  1. maquetacion(&CardData) -> (Maquetacion, bool): how the whole card is composed. The bool says "layout unknown" (falls back to Normal and the painter adds Aviso::LayoutDesconocido).
  2. cuerpo(&CardData, cara) -> Cuerpo: what goes inside the rules box of one face. It comes from that face's type line (a transforming card can be a saga in front and a planeswalker behind), and otherwise from the card.

Why two: Scryfall's layout is not enough. Aftermath, Fuse and Room cards all arrive as split; battles arrive as transform; planeswalkers, dungeons and Station cards arrive as normal. So the code looks at keywords, the English type line and the rules text.

flowchart TD
  CD["CardData (Scryfall snapshot)"] --> M["maquetacion(): Maquetacion"]
  CD --> C["cuerpo(cara): Cuerpo"]
  M --> R["componer::render"]
  R -->|simple face| CS["cara_simple"]
  R -->|split / fuse / room| P["partida"]
  R -->|aftermath| S["secuela"]
  R -->|flip| F["flip"]
  R -->|battle front / plane| A["apaisada"]
  R -->|meld back| MD["meld_dorso"]
  R -->|scheme / vanguard| G["direct, big canvas"]
  CS --> PC["render::pintar_cara"]
  P --> PC
  S --> PC
  F --> PC
  A --> PC
  MD --> PC
  G --> PC
  C --> PC
  PC --> CU["cuerpos: contenido, ajustar, pintar_contenido"]
  CU --> ES["estructura: parse rules text"]
Loading

Maquetacion and maquetacion()

Maquetacion has 27 variants. The mapping (from maquetacion()):

Scryfall layout (plus rule) Maquetacion Printed faces
split + keyword Fuse Fusionar 1
split + keyword Aftermath Secuela 1
split + a face type contains word Room Habitacion 1
split (otherwise) Partida 1
flip Flip 1
adventure Aventura (omens use this too) 1
transform or battle, first face type has word Battle Batalla 2
transform Transformable 2
modal_dfc Mdfc 2
meld Meld 2
reversible_card Reversible 2
double_faced_token with Dungeon Mazmorra 1
double_faced_token FichaDosCaras 2
leveler, class, case, saga, prototype, mutate, emblem Leveler, Clase, Caso, Saga, Prototipo, Mutar, Emblema 1
planar / scheme / vanguard Plano / Plan / Vanguard 1
token Ficha 1
normal + type Dungeon Mazmorra 1
normal + type Planeswalker Planeswalker 1
normal + Station keyword, or a line starting Station ( / Station. Estacion 1
normal + CardData::es_ficha() Ficha 1
normal (otherwise) Normal 1
anything else Normal + "unknown layout" flag 1

Helpers on Maquetacion: caras_impresas() (2 for transform-likes, 1 otherwise), es_grande() (plane, scheme, vanguard), es_partida() (split, fuse, room). The free function caras_impresas(&CardData) caps the number at the faces actually present (the fused card of a meld designed alone is one face).

Cuerpo and cuerpo()

Cuerpo Chosen when Content
Lealtad face type has word Planeswalker loyalty abilities with a shield badge
Capitulos Saga chapters with numerals
Niveles Class class levels
Caso Case "To solve / Solved"
Mazmorra Dungeon a floor plan of the rooms, like the printed dungeons (see Dungeon floor plan)
Leveler layout leveler level ranges with P/T
Prototipo layout prototype prototype box
Estacion face 0 of a Station card thresholds `8+
Texto default rules and flavour

The type-line match is by whole word (tiene_palabra), case-sensitive, on the English type line.

maquetacion::resumen(design, cara, lang) produces a Resumen { maquetacion, caras_impresas, partes: Vec<Parte> } that the editor shows ("how this face will be laid out"); each Parte has marca, cabecera, texto, fr. It re-runs the same estructura parsers, so the UI never duplicates parsing. It returns an empty partes when the text follows no pattern (or has fewer than two sections).

componer.rs: assembling cards

Entry points: render(motor, e, cara, lang, rec, card, ancho_px, salida), render_capa(...) and medida(e, card). Everything is built with render::pintar_cara on canvases of the right shape and pasted rotated (pegar with Giro::{Nada, Izquierda, Derecha, Media}; rotation by 90 degrees to the left, 90 degrees to the right or 180 degrees; a 1:1 pixel paste of a rotated piece uses nearest-neighbour so text is not blurred). "One engine for everything" is the stated design.

render computes medida first: medida_grande() = 88.9 x 127 mm only when the card is a big layout and CardData.oversized is true, otherwise the deck's card size. Then it dispatches:

Case Function How it is built
Normal, tokens, transform/MDFC/battle back, reversible, saga, class, planeswalker, adventure… cara_simple One pintar_cara call. Chooses the pack layout with layout_de_cara (below). Adds a face indicator for Transformable/Batalla/Meld (sun = face 0, moon = face 1) and Mdfc (front/back triangles plus the other-face strip with its type line and cost, left out when it would say nothing new: both faces have the same type line and the other costs nothing, as in the two-land Pathways; pestana_redundante, which also adds `
Split, Fuse, Room partida Two mini cards rendered with the normal style and rotated 90 degrees left into the two halves of the card (first half at the bottom, second at the top, so turning the card right reads left to right). Fuse reserves a 6 mm strip (franja_fusionar, pack layout fusionar, 88 x 6) on the right and removes the Fuse line from the halves by writing a temporary rules_override that is not reported as "hand-edited". The Fuse line is recognised in any design language (textos_motor::es_linea_fusionar); the strip shows the first half's Fuse line, or textos_motor::recordatorio_fusionar(lang) if it has none.
Aftermath secuela Top half normal with pack layout secuelaArriba (63 x 51 mm, no footer); bottom half with secuelaAbajo (33 x 63 mm) rotated right (Giro::Derecha), as on the printed Amonkhet cards: its title runs down the card's right edge, then its artwork, type line and rules towards the left (split halves turn the other way, as Fire // Ice shows). It has its own artwork: design face 1's art ("Part 2" in the Art tab); the footer comes from pie_suelto. A pack whose secuelaAbajo still has a 0 x 0 arte simply paints no artwork there (the painter skips an empty art window), so older packs keep working.
Flip flip Face 0 on the whole card with layout flip; face 1 on a transparent canvas without artwork, rotated 180 degrees; footer from pie_suelto. The pack's flip layout only draws the top half.
Battle front, Plane apaisada Painted landscape (canvas swapped: width = card height) with layout batalla (88 x 63) or plano (127 x 88.9), then rotated left onto the portrait card.
Scheme, Vanguard direct pintar_cara with layouts plan / vanguard (88.9 x 127), no rotation.
Meld back meld_dorso Paints the melded card (design face 1) on a vertical canvas twice as tall as wide (fw = card height, fh = 2 x card width), keeps the top or bottom half according to CardData.meld_arriba, rotates it left.

Artwork per piece follows the printed cards: the two halves of split, Fuse and Room cards and of aftermath cards each show their own design face's artwork (from Scryfall, each half is cut out of the card's single double art_crop, see Designer-App-Layer and Imaging-and-PDF); flip, adventure and Preparar cards share one artwork (face 0's); transform, MDFC, battle and meld faces each have theirs. Only the Fuse strip and the loose footer are painted without artwork.

pie_suelto paints only the footer by cloning the design with empty rules and flavour and the pack's pie layout (63 x 88). If the pack has no pie (or fusionar) layout the piece is simply skipped. (Both exist in the builtin pack.)

Choosing the pack layout (layout_de_cara)

For cara_simple the layout name is, in order:

  1. saga if the body is Capitulos,
  2. mazmorra if Mazmorra,
  3. ficha if the card is a token or the composition is Emblema / FichaDosCaras,
  4. alto ("taller rules box") for bodies with sections: Lealtad, Niveles, Caso, Leveler, Estacion,
  5. none (plain style) otherwise (prototype, mutate, normal…).

render::estilo_para then resolves the name: a layout of the style (EstiloDef.layouts) modifies the style; a complete layout of the pack (PackDef.layouts) replaces it; a missing one falls back to the builtin pack's complete layout; if none exists anywhere, Aviso::PackSinLayout (except for alto, which is optional and silently falls back to the normal box). A pack without a ficha layout for a style uses the builtin pack's token style with no warning. Details in Frame-Packs.

render_capa and live artwork

render_capa(motor, e, cara, …) gives the live-drag layer of a face or part (cara is the design face whose artwork moves, e.g. part 2 of an aftermath card). For a simple face it paints the face with sin_arte + transparente. For composed and big cards it runs the same piece assembly as render (componer_pieza, shared by both) with capa = Some(parte): the composite canvas starts transparent instead of black (the screen draws the black), that part's piece is painted with sin_arte + transparente, every other piece normally, and the piece reports where its art window lands on the card. That position comes from the same transform pegar used to paste the piece (pegar returns it; hueco_pegado maps the window's corners), and Pieza.arte_giro says how the artwork is seen in it: 0, 90 (aftermath bottom half, turned right) or -90 (split, Fuse and Room halves, battle front, planes). The fade extension is only kept for unrotated pieces. It returns CapaVista { imagen, arte, extension, fondo, radio, cara, giro } after the corner mask, or None when the part has no artwork of its own (maquetacion::arte_propio: part 2 of a flip, adventure or Preparar card shows part 1's) or for the meld back (still painted whole). Why: without it every drag step on a composed card asked Rust for the whole card and lagged. The test las_compuestas_tienen_capa_de_cada_parte_con_su_giro mounts each layer the way the screen does (black, a flat red artwork in the hole, the layer on top) and compares it with the full render, for both parts of split and aftermath cards and for flip.

estructura.rs: parsing the rules text

All patterns accept the English Oracle text and the Spanish printing; class levels, case headers and leveler ranges also accept every other printed language (see the table). Text that does not follow its pattern stays normal text (callers check "fewer than two sections" and fall back). limpiar() first removes lines that are only a marker like //Level_2// that Scryfall leaves in some printings.

Function Pattern Output
planeswalker line starts with +1:, -2:, 0:, +X: (sign may be + - - – −; minus is normalised to the typographic minus) Vec<Habilidad { coste: Option, texto }>; lines without a cost are static abilities
saga line starts with Roman numerals joined by commas, then a dash (I, II — text); lines before the first chapter are the intro; lines after a chapter without numeral continue it Saga { intro, capitulos: Vec<Capitulo { numeros, texto }> } (also a_romano)
clase a line {cost}: Level N starts a level, with the level word of any printed language (Nivel, Niveau, Stufe, Livello, Nível, Уровень, Рівень, Poziom, レベル, 等级, 等級; Korean puts the number first, 2레벨; full-width digits and : accepted), from Wizard Class in each language (NIVEL_CLASE); the first level has no header Vec<Tramo>
caso lines beginning with a case header plus a dash (To solve / Solved, Resolver / Para resolver / Para resolverlo / Resuelto, À résoudre / Résolue, Lösen / Gelöst, Risolvere / Risolto, 解明条件 / 解明完了, 侦办 / 侦结…, from Case of the Shattered Pact; Chinese uses ~ as the dash) Vec<Tramo>
leveler the level word of any language (textos_motor::palabras_nivel: LEVEL, NIVEL, STUFE, 等级…) plus 2-6, or 8+; a following a/b line is that range's P/T Vec<Tramo { cabecera, fr, texto }>
estacion `8+ text; text that looks like P/Tbecomes the threshold'sfr`
prototipo Prototype {1}{B}{B} — 3/3 (...) or the Spanish layout (cost line, Prototipo. (...), loose P/T) Prototipo { coste, fr, recordatorio, resto }
mazmorra Room name — text (Leads to: A, B) / (Lleva a: ...) Vec<Sala { nombre, texto, lleva_a }>
nota_de_mazmorra every non-empty line that is not a room (Undercity's "You can't enter this dungeon unless…") Vec<String>, drawn above the map
plano_de_mazmorra the floor plan of the rooms (below) Plano { filas, sitios: Vec<SitioSala { fila0, fila1, x0, x1 }>, puertas: Vec<Puerta { fila, x }> }; x0, x1, x are fractions of the width (0 to 1), fila0..=fila1 the rows a room spans, a door sits in the wall under row fila
tramos_libres the spans of [0, 1] not covered by the given ones Vec<(f32, f32)>; shared by the plan and the painter (rock, walls)

Dungeon floor plan

Printed dungeons are not flowcharts: the rooms are blocks that touch, separated by walls, and you go from one to the next through a door. plano_de_mazmorra(&[Sala]) -> Plano rebuilds that layout from nothing but the room list and each room's "Leads to":

  1. Destinations are matched by room name (a room leading to itself is ignored). If no room leads anywhere (Baldur's Gate Wilderness, whose rooms are all entered from outside), rejilla returns a grid without doors: three rooms per row (two with 3–4 rooms, one with 1–2), the last row sharing the full width.
  2. Row of a room = its longest path from the first room (repeated relaxation, at most n passes: official dungeons are acyclic, the cap only guards against a hand-written cycle).
  3. Rows spanned: a room extends down to the row above its nearest destination; a room with no destination (the final room) extends to the bottom. Why: on the printed Tomb of Annihilation, Oubliette leads straight to the final room and spans the height of Veils of Fear plus Sandfall Cell beside it; without this the plan would have a hole.
  4. Horizontal placement, row by row: the rooms that start in a row share equally the free spans left by the rooms coming down from above (tramos_libres). Each new room goes into the free span nearest to the mean centre of the rooms that lead to it, and inside a span the rooms are ordered by that centre, so a room sits under the room it comes from. If the rooms from above leave no free span at all, they are cut to end in the previous row and the new row gets the full width.
  5. Doors: one per step from a room to a room that starts right below its last row, in the middle of the span they share or, if they do not overlap, at the edge of the upper room nearest to the destination (kept slightly inside it). Two doors at the same spot are merged.

Tomb of Annihilation comes out as on the card (▼ = door; the gaps in the top and bottom walls are the entrance and the exit):

┌──────────────   ──────────────┐
│         Trapped Entry         │
├──────▼────────┬────────▼──────┤
│ Veils of Fear │               │
├──────▼────────┤   Oubliette   │
│ Sandfall Cell │               │
├──────▼────────┴────────▼──────┤
│    Cradle of the Death God    │
└──────────────   ──────────────┘

Tests in estructura.rs: la_mazmorra_del_mago_loco_por_filas (Dungeon of the Mad Mage: 7 rows, the two middle rooms in halves, door positions), la_oubliette_ocupa_dos_filas_como_en_la_carta, las_salas_de_phandelver_se_abren_donde_se_tocan (doors in the middle of shared spans), undercity_con_su_nota_y_sin_ella_en_las_salas (the note is not a room) and sin_conexiones_las_salas_van_en_rejilla.

cuerpos.rs: sections with badges

Model

  • Contenido (what to show): Texto { reglas, sabor }, Secciones { secciones, sabor, repartir, rayas }, Aventura { cabecera, tipo, izq, der, sabor }.
  • Seccion { izq: Option<Insignia>, cabecera, parrafos, fr, banda }.
  • Insignia: Lealtad(String), Capitulos(Vec<u32>), Nivel { etiqueta, valor }, Prototipo { coste, fr }. Each has a column width (6.4 / 4.8 / 6.4 / 9.6 mm) and a minimum section height.
  • Hecho is the same content laid out at a font size (Texto(CajaTexto), Secciones { items: Vec<SeccionHecha>, sabor, alto, tam }, Aventura { ... }). Hecho::fin_ultima_linea() reports how far right the last line reaches so the painter can keep clear of the power/toughness box (a section with its own P/T box counts as reaching the right edge).

contenido(cuerpo, reglas, sabor, lang)

Builds the Contenido; anything not matching returns plain Texto.

Body Result
Lealtad one section per ability, badge = loyalty cost, alternating banda, repartir: true, no dividers
Capitulos optional intro section, then one section per chapter with a numerals badge; repartir, rayas
Niveles / Caso sections headed by the level / "Solved" header (bold, can contain symbols); needs at least two sections
Leveler sections with a Nivel badge (the design language's word, textos_motor::palabra_nivel(lang)) and the range's P/T; no repartir
Estacion like leveler without the label word
Prototipo a banded section with the prototype box and reminder, plus a section for the rest
aventura(...) Contenido::Aventura: left column = adventure (name and cost, italic type, rules), right = creature rules and flavour

ajustar and maquetar

maquetar lays a Contenido out at one size; ajustar is the shared "shrink until it fits" loop: for Texto it delegates to texto::ajustar_reglas; for sections and adventure it binary-searches the size in 0.1 pt steps to the minimum, then tightens spacing, and finally reports overflow (sobra). render.rs calls it with the box height from the pack's dynamic box or fixed hueco. Section geometry: per-section padding 0.24 x size, left column from the badge width plus 0.8 mm gap, a 7 mm column on the right when the section has P/T, minimum height from the badge. The adventure book takes 48 % of the width.

Painting (Carta::pintar_contenido)

  • Banded sections get a 22/255 alpha wash of the ink colour; dividers are 110/255 ink lines. Sections with repartir share the leftover height equally; the flavour text is pinned to the bottom with a divider above it.
  • Badges (Carta::insignia): loyalty is a pentagon-ish shield (arrow up for +, down for the minus sign, flat for 0); chapters are small hexagons stacked per numeral; levels are a pointed tag with the label above the value; prototype is a rounded rectangle with the cost symbols and a small P/T box. Colours come from the pack's insignias (fondo, borde, texto), defaulting to near-black with the frame colour border and white text; pngLealtad replaces the loyalty shape with an image.
  • Section P/T boxes (caja_fr_tramo) sit at the right edge.
  • The adventure book is a translucent page with a border, with the adventure's name/cost/type above its rules.
  • pintar_mazmorra: the dungeon map, below.

Painting a dungeon (pintar_mazmorra)

render::pintar_cara calls Carta::pintar_mazmorra(nota, salas, caja, est, tinta) when the body is Mazmorra and the rules text yields rooms, with the reglas hueco in mm (the mazmorra pack layouts make it the whole area from under the title to above the footer: no artwork and no type line, like the printed dungeons), the hueco's text style and the box ink (tintaCaja). The normal rules content is then empty, so nothing else is drawn in the box.

  • Note: the nota_de_mazmorra lines go in italics at 90 % of the size, centred, across the top; the map takes the remaining height.
  • One font size for every room, the largest at which everything fits. A row is as tall as its single-row rooms need (bold name at 1.12x the size, then the rules, plus wall and MARGEN_SALA on each side, a shared wall counting half), at least one line tall; a room spanning several rows that still lacks height spreads the shortfall over them. Attempts, in order: the style's size with the name above the text; the same size with inline rooms (a room whose text is one paragraph that fits beside its name is drawn as "Name text" on one line with aligned baselines, as the printed Dungeon of the Mad Mage shows "Dungeon Level Scry 1."), because shrinking every room is worse than joining a few short ones; then a binary search of the size in 0.1 pt steps down to 55 %; if it still does not fit, the minimum size and Aviso::TextoNoCabe { hueco: "reglas" } with the overflow in mm.
  • Spare height is shared out in proportion to each row's need, so one-line rows stay corridors and big rooms stay big.
  • Drawing (one SVG through forma): a tiled floor (LOSA, 2.2 mm squares in ink at 10 % opacity) over the layout's own box background; rock in the wall colour wherever no room is in a row; walls (PARED, 1.05 mm) between rows (open where a room continues down and at doors), between rooms of one row, and around the map. The outer wall is open at the entrance (top of the first room) and at the exits (bottom of the rooms without "Leads to"), only when the dungeon has doors; a grid dungeon is closed. Doors are PUERTA (3 mm) gaps holding a small white triangle pointing down, towards the next room.
  • Wall colour: the palette variable oscuro on a light floor, medio on a dark one (fallback #2c2c2c). Light or dark is read from the ink (render::luminancia(tinta) < 0.5 means dark text, so a light floor), which keeps the walls contrasting with whatever box colour the user picked.
  • Texts are centred in each room's inner box.

Card layout coverage

Every layout is covered by a golden image and by real card data in src-tauri/crates/proxy-core/tests/dorados/layouts/ (images) and tests/dorados/layouts/datos/*.json (card snapshots in the app's CardData JSON format, fetched from Scryfall by the ignored test borrador::en_vivo::datos_de_layouts in src-tauri/src/diseno/borrador.rs). tests/layouts.rs → CASOS renders each in the given style and asserts that no PackSinLayout / LayoutDesconocido warning appears; multi-face cards produce -a/-b images.

Data file Card used Scryfall layout Style rendered
pw-3 / pw-4 Liliana of the Veil / Jace, the Mind Sculptor normal (planeswalker) Borderless / Extended
saga-urza / saga Urza's Saga / The Eldest Reborn saga Borderless
clase Artificer Class class Borderless
caso Case of the Locked Hothouse case Classic
partida Fire // Ice split Borderless
fusionar Turn // Burn split + Fuse Borderless
secuela Dusk // Dawn split + Aftermath Borderless
habitacion Bottomless Pool // Locker Room split (Room) Borderless
aventura / presagio Bonecrusher Giant / Bloomvine Regent adventure Borderless / Extended
flip Akki Lavarunner flip Borderless
transform Delver of Secrets transform Borderless
mdfc Valakut Awakening modal_dfc Borderless
batalla Invasion of Zendikar transform (battle) Borderless
meld Bruna, the Fading Light meld Borderless
reversible Ajani Goldmane reversible_card Borderless
ficha2 Angel // Demon token double_faced_token Borderless
leveler Student of Warfare leveler Borderless
prototipo Phyrexian Fleshgorger prototype Borderless
mutar Gemrazer mutate Borderless
estacion Atmospheric Greenhouse normal + Station Borderless
emblema Ajani, Adversary of Tyrants Emblem emblem Borderless
mazmorra Dungeon of the Mad Mage normal (dungeon) Borderless
plano Akoum planar (oversized) Borderless
plan All in Good Time scheme Borderless
vanguard Ashnod vanguard Borderless

Also tested: las_cartas_grandes_salen_a_su_medida renders plane, scheme and Vanguard at 889 px wide and asserts 889 x 1270 (88.9 x 127 mm), and checks that Fire // Ice classifies as Partida. The base (non-layout) cases are in tests/dorados.rs, see Designer-Engine. Unit tests in maquetacion.rs (la_tabla_de_layouts walks the classification table row by row), componer.rs (including the rotations, e.g. girar_a_la_derecha_deja_lo_de_arriba_a_la_derecha), cuerpos.rs and estructura.rs cover the rest.

An ignored matrix test, balatro_matriz in tests/layouts.rs, renders every layout above with every own style and variant of the downloadable Balatro pack, in Spanish and English, and fails on any PackSinLayout, LayoutDesconocido or EstiloNoEnElPack (text that does not fit is only counted). It needs PB_BALATRO=<pack folder>.

Big cards and the page plan

Planes, schemes and Vanguard have a physical size of 88.9 x 127 mm (3.5 x 5 in). The designer renders them at that size only if Scryfall marks them oversized; the app then places them on dedicated sheets (big_grid, big_cells, Page.big in the PagePlan). See Layout-Engine.

Gotchas

  • A new composition needs three coordinated changes: the classification in maquetacion(), a branch in componer::render (and es_simple if it is not a simple face), and a layout in the builtin pack and in any pack that should support it (otherwise PackSinLayout).
  • Because composed cards call pintar_cara several times with a cloned design, warnings are de-duplicated in render and temporary rules_override edits must not leak a ReglasEditadas warning (see the Fuse handling in partida).
  • cuerpo() is per face but maquetacion() is per card; a transform card can have two different bodies.
  • Structure parsing depends on printed keywords. Class levels, case headers and leveler ranges know every printed language; prototype (Prototipo) and dungeon rooms (Lleva a) only know English and Spanish. Printings with other wording fall back to plain text.
  • Scryfall has no printing of any dungeon in a language other than English, so a dungeon's rooms are always read from the English text unless the user writes the rules by hand (in English or with the Spanish (Lleva a: …)).

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

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

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally