State model (Store<Dashboard> via context), bundled item data, SSR/hydrate pipeline, component tree, conventions, and current gaps.
7.1 KiB
ARCHITECTURE
This document describes how aex works today. It is a snapshot, not a design
spec: several pieces are stubs or unwired (see "Current gaps" at the end and
the phased plan in ROADMAP.md).
What it is
A D&D 5e character sheet and campaign dashboard for one party's play-by-text game. The UI is in Russian; code and comments are in English. The app tracks the campaign (name, in-game date, session number), one player character (identity, class, HP, money, spell slots, inventory, hit dice, death saves), a party-shared stash, and — in the data model only, no UI yet — quests, notes, and contacts.
Stack
- Rust, edition 2024, nightly toolchain (
rust-toolchain.toml, channel pinned tonightlywithout a date) - Leptos 0.8.20 (SSR + hydration), reactive_stores 0.4.3 for state
- leptos_actix 0.8.7 + actix-web 4 for the server
- nucleo-matcher for fuzzy item search, num-format for number display, chrono for dates, lazy_static for one-time data loading
- SCSS sources in
style/(compiled by cargo-leptos) - Bundled game data in
data/(5e.tools-format JSON, ~3.9 MB total)
Features: ssr (server binary), hydrate (WASM), csr (optional pure-client
mode for trunk). Cargo.lock is gitignored.
Directory layout
src/
main.rs SSR entry point: actix-web server, favicon
lib.rs crate root, module declarations, WASM hydrate()
app.rs App component: context, shell, routes
prelude.rs crate-wide re-exports and the Context alias
state/ App state (one Store<Dashboard> in context)
entities/ Domain model (Store-derived structs) + mock data
statics/ Bundled item data: ITEMS_REFS, ITEMS_NAMES
utils/ read_items, adjust_checked, BasisPoints trait
components/
header.rs Campaign header (name, date, session, Save/Load/Clear)
nav.rs Route links
sidebar.rs Character sidebar, composes the seven panels below
sidebar/*.rs image, names, about, hit_points, hit_dice,
death_saving_throws, spell_slots
inventory.rs One inventory section (rows + money)
item.rs One inventory row
numput.rs Numeric input components (Numput, NumputChange)
mod.rs Pages (Character, Inventories, Wiki) + item search
State model
The whole dashboard is one reactive tree:
entities::Dashboardis the root struct: campaign fields,player: PlayerData,common: CommonData, plus quest/note/contact books and settings.- Every entity struct derives
reactive_stores::Store, so every nested field is independently reactive (copy-on-write paths). AppcreatesStore::new(Dashboard::mock())and provides it as Leptos context. Components read it withexpect_context::<Context>()and grab field handles, e.g.let hp = state.player().hp();.- Writes go through
field.update(|v| ...),field.set(...), orfield.write(). All arithmetic is saturating (saturating_add/sub,checked_add_signed) so values never underflow.
The state is mock data only — Dashboard::mock() runs on every load and
nothing is persisted. The header's Save/Load/Clear buttons do nothing yet
(ROADMAP phase 1).
Data layer (items)
statics::ITEMS_REFSis alazy_staticmap of name →BookItem, built once byutils::read_itemsover the bundled JSON files (items.json,base.json,foundry.jsonloaded twice — a known duplication — andmagic.json), roughly 5k items.BookItemkeepsname,weight,rarity, andrest, which holds all other JSON fields verbatim for future detail views.- Duplicate names merge: the later entry fills a missing weight and appends
its
restfields. ITEMS_NAMESis the flat name list fed to the fuzzy matcher.
Weights are stored as u32 basis points (100 = 1.0) in inventory items and
formatted with the utils::BasisPoints trait; book items use f64 from the
JSON as-is.
Rendering pipeline
main.rs(featuressr) starts an actix-web server, generates the route list fromApp, and renders each route server-side throughleptos_routes. It serves the WASM/CSS from/pkgand static assets from/assets.- The WASM bundle (
hydratefeature) callshydrate()inlib.rs, which mountsAppwithhydrate_bodyover the server-rendered DOM. - Routing:
/→ Character page,/inv→ Inventories,/wiki→ Wiki, anything else →NotFound(sets HTTP 404 during SSR).
Component tree
App
├─ Header name, image, date steppers, session steppers, Save/Load/Clear (dead)
├─ Sidebar
│ ├─ Image portrait
│ ├─ Names first / alias «» / last
│ ├─ About class, level ±, XP
│ ├─ HitPoints current/max with bar, temp HP
│ ├─ HitDice die-kind select, per-level clickable dice
│ ├─ DeathSavingThrows fail/success pips + reset
│ └─ SpellSlots 9 levels of clickable pips
├─ Nav links to the three routes
└─ Routes
├─ Character stub text
├─ Inventories two Inventory sections (Personal/Common)
│ └─ Inventory balance (5 × Numput), rows (ForEnumerate)
│ └─ Item name, quantity ±, weight ±, more/swap/remove
└─ Wiki stub text
The inventories page reads Context, splits it into Field<Inventory> and
Field<Balance> handles, and passes them down. Item rows get move/remove
closures by index; "move" transfers a row between the personal and common
inventories, "remove" deletes it. The "…" (details) button and the search
"+" button are unwired.
Numput is the shared numeric input: +n/-n adjust relative to the
current value, a plain number sets absolutely, invalid input is reverted,
and the value renders with thousands separators. NumputChange is the same
pattern with caller-supplied display and parse functions (used for weights
in basis points).
Item search (ItemSearchField) re-runs a nucleo fuzzy match on every
keystroke over ITEMS_NAMES and renders ItemSearchCards. It is not
currently mounted on any page.
Conventions
- UI strings are Russian and hardcoded in the views; ids like
id="hp"exist for e2e tests. - Collections come from
std::collectionsvia the prelude (HashMap,HashSet). Forlists are keyed: inventories use#[store(key)]onrow.id, sidebar pips key by index.- Logging goes through the
logmacros re-exported in the prelude (info!,warn!, ...).
Current gaps
- No persistence; state is recreated from
mock()each load CharacterandWikipages render placeholders- Quests, notes, contacts: models and mock data only
- Prepared spells list is empty;
Spellis a name-only stub; attunements have no UI;Settingsis empty - Save/Load/Clear buttons and the item "+"/"…" buttons are dead
foundry.jsonis loaded twice; ~3.9 MB of JSON is bundled into the WASM and parsed on startup- Several unused dependencies (
leptos-use,dotenvy,pretty_env_logger,seq_macro,itertools,thiserror) and 12 compiler warnings Cargo.lockis gitignored, so builds are not reproducible
See ROADMAP.md for the ordered plan to fix these.