diff --git a/src/app.rs b/src/app.rs index e758e77..352cf47 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,6 +17,12 @@ use crate::{ prelude::* }; +/// Root component of the app. +/// +/// Sets up meta context, creates the `Store` (mock data for now) +/// and injects it as context, then renders the shell: header, sidebar, nav +/// and the routed page. Routes: `/` character, `/inv` inventories, +/// `/wiki` wiki; anything else falls to `NotFound`. #[component] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. diff --git a/src/components/header.rs b/src/components/header.rs index e4832c6..8cafbdc 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -2,6 +2,9 @@ use crate::prelude::*; use chrono::TimeDelta; use leptos::prelude::*; +/// Campaign header: editable name, campaign image, in-game date and +/// session steppers, and Save/Load/Clear buttons (not wired yet — see +/// ROADMAP phase 1). #[component] pub fn Header () -> impl IntoView { let state = expect_context::(); @@ -10,10 +13,12 @@ pub fn Header () -> impl IntoView { let session = state.session(); let image = state.campaign_image(); + // Shifts the in-game date by ±1 day (saturating, via checked_add_signed). let adjust_day = move |adjustment: i64| date.update(|d| { *d = d.checked_add_signed(TimeDelta::days(adjustment)).unwrap(); }); + // Shifts the session counter by ±1, guarding against underflow. let adjust_session = move |adjustment: isize| session.update(|s| { if let Some(new) = s.checked_add_signed(adjustment) { *s = new; diff --git a/src/components/inventory.rs b/src/components/inventory.rs index 01b818f..61d5415 100644 --- a/src/components/inventory.rs +++ b/src/components/inventory.rs @@ -2,6 +2,9 @@ use leptos::{logging, prelude::*}; use reactive_stores::Field; use crate::{ components::{ item::Item as ItemRow, numput::Numput }, prelude::* }; +/// One inventory section: a titled balance block (five `Numput`s) and the +/// item rows. `kind` decides personal/common styling and the move +/// direction. #[component] pub fn Inventory ( kind: InventoryKind, @@ -11,10 +14,12 @@ pub fn Inventory ( let state = expect_context::(); + // Removes the row at `idx` from this inventory. let rm = move |idx: usize| inventory.update(|v| { v.rows.remove(idx); }); + // Moves the row at `idx` to the other inventory (personal ↔ common). let mv = move |idx: usize| { let (from, to): (Field, Field) = match kind.dst() { InventoryKind::Personal => (state.common().inventory().into(), state.player().inventory().into()), diff --git a/src/components/item.rs b/src/components/item.rs index 1a720ec..53193c0 100644 --- a/src/components/item.rs +++ b/src/components/item.rs @@ -3,6 +3,9 @@ use leptos::prelude::*; use reactive_stores::{Field, OptionStoreExt}; +/// One inventory row: name, quantity stepper + `Numput`, weight stepper + +/// `Numput`, and more/swap/remove buttons. A weight-based tag drives the +/// CSS class. The "…" button is not wired yet (ROADMAP phase 3). #[component] pub fn Item ( idx: ReadSignal, @@ -16,10 +19,14 @@ pub fn Item ( let empty = String::new(); + // CSS class from the item's weight tag ("weight" key). let class = move || format!("item {}", item().tags().get().get("weight").unwrap_or(&empty)); + // Weight shown as a decimal: basis points → "x.yy". let weight_display = move || item().weight().with(|f| f.to_decimal_string()); + // Parses weight input: "+n"/"-n" adjust relative, plain numbers set + // absolutely, anything else restores the current value. let weight_change = move |ev: Targeted| { let old = item().weight().get_untracked(); let new = ev.target().value(); diff --git a/src/components/mod.rs b/src/components/mod.rs index 38922cd..1d73e2a 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,9 @@ +//! UI components. +//! +//! `header`, `sidebar` and `nav` form the app shell; `inventory`, `item` +//! and `numput` implement the inventories page; the page components +//! (`Character`, `Inventories`, `Wiki`) and the item search live in this +//! module root. use leptos::prelude::*; use nucleo_matcher::{ pattern::{ CaseMatching, Pattern, Normalization }, Config, Matcher }; use reactive_stores::Field; @@ -11,6 +17,8 @@ pub mod inventory; pub mod numput; pub mod item; +/// Character page (route `/`). Stub: renders a placeholder until +/// ROADMAP phase 2. #[component] pub fn Character () -> impl IntoView { view! { @@ -18,6 +26,8 @@ pub fn Character () -> impl IntoView { } } +/// Inventories page (route `/inv`): the player's and the party's +/// inventories side by side, each with money and item rows. #[component] pub fn Inventories () -> impl IntoView { let state = expect_context::(); @@ -40,6 +50,8 @@ pub fn Inventories () -> impl IntoView { } } +/// Wiki page (route `/wiki`). Stub: renders a placeholder until +/// ROADMAP phase 7. #[component] pub fn Wiki () -> impl IntoView { view! { @@ -47,6 +59,11 @@ pub fn Wiki () -> impl IntoView { } } +/// Fuzzy item search over the bundled item names. +/// +/// Re-matches on every keystroke (nucleo) and renders result cards. The +/// "+" buttons on the cards are not wired to the inventory yet (ROADMAP +/// phase 3). #[component] pub fn ItemSearchField () -> impl IntoView { let (query, set_query) = signal("".to_owned()); @@ -86,6 +103,7 @@ pub fn ItemSearchField () -> impl IntoView { } +/// One search result: name, weight, rarity and an (unwired) add button. #[component] pub fn ItemSearchCard <'a> (item: &'a BookItem) -> impl IntoView { let card_class = format!("item-search-card {}", item.rarity.as_ref().unwrap_or(&String::new())); diff --git a/src/components/nav.rs b/src/components/nav.rs index 52b3328..d81084c 100644 --- a/src/components/nav.rs +++ b/src/components/nav.rs @@ -2,6 +2,7 @@ use crate::prelude::*; use leptos::prelude::*; use leptos_router::components::A; +/// Top navigation: links to the three routes (character, inventory, wiki). #[component] pub fn Nav () -> impl IntoView { view! { diff --git a/src/components/numput.rs b/src/components/numput.rs index 47bc70a..3abeb33 100644 --- a/src/components/numput.rs +++ b/src/components/numput.rs @@ -5,6 +5,11 @@ use num_format::{Locale, ToFormattedString}; use num_traits::{ SaturatingSub, SaturatingAdd }; use crate::prelude::*; +/// Number input bound to a reactive field. +/// +/// `+n` / `-n` adjust the current value relatively, a plain number sets it +/// absolutely, and anything else is rejected (the input is restored). The +/// value renders with thousands separators (`num_format`). #[component] pub fn Numput ( field: U, @@ -37,6 +42,11 @@ pub fn Numput ( } } +/// `Numput` with caller-supplied display and change handlers. +/// +/// For fields whose display differs from the stored value (e.g. weights +/// in basis points): `field_with` renders the input, `change` parses the +/// edit. #[component] pub fn NumputChange ( mut field_with: U, diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs index f1d3ff4..9e478a1 100644 --- a/src/components/sidebar.rs +++ b/src/components/sidebar.rs @@ -1,3 +1,5 @@ +//! Character sidebar: portrait, names, class/level/XP, HP, hit dice, +//! death saves and spell slots. use crate::prelude::*; use leptos::prelude::*; @@ -9,6 +11,8 @@ mod hit_dice; mod death_saving_throws; mod spell_slots; +/// Left-hand character panel. Reads `Context` and stacks the sub-panels +/// (image, names, about, HP, hit dice, death saves, spell slots). #[component] pub fn Sidebar () -> impl IntoView { view! { diff --git a/src/components/sidebar/about.rs b/src/components/sidebar/about.rs index 4b5e2dc..19dce0f 100644 --- a/src/components/sidebar/about.rs +++ b/src/components/sidebar/about.rs @@ -1,6 +1,7 @@ use crate::prelude::*; use leptos::prelude::*; +/// Class (text input), level (+/− steppers) and XP (number input). #[component] pub fn About () -> impl IntoView { let state = expect_context::(); @@ -10,12 +11,14 @@ pub fn About () -> impl IntoView { let level = player.level(); let xp = player.xp(); + // Steps the level by ±1, guarding against overflow. let adjust_level = move |adjustment: i8| level.update(|l| { if let Some(new) = l.checked_add_signed(adjustment) { *l = new; } }); + // Applies the XP input with checked parsing (invalid text is reverted). let adjust_xp = move |ev: Targeted| { utils::adjust_checked(ev, xp); }; diff --git a/src/components/sidebar/death_saving_throws.rs b/src/components/sidebar/death_saving_throws.rs index 6338035..04b28f5 100644 --- a/src/components/sidebar/death_saving_throws.rs +++ b/src/components/sidebar/death_saving_throws.rs @@ -1,6 +1,8 @@ use crate::prelude::*; use leptos::prelude::*; +/// Three failure pips and three success pips, plus a reset button. +/// Clicking a pip fills it; clicking the last filled pip empties it. #[component] pub fn DeathSavingThrows () -> impl IntoView { let state = expect_context::(); @@ -8,6 +10,7 @@ pub fn DeathSavingThrows () -> impl IntoView { let dt = player.death_save_throws(); + // Toggles pip `slot_id` of the fail/success track. let process_dt_click = move |success: bool, slot_id: u8| { dt.update(|d| { let v = if success { &mut d.succeeded } else { &mut d.failed }; diff --git a/src/components/sidebar/hit_dice.rs b/src/components/sidebar/hit_dice.rs index ce33f26..d02a2b5 100644 --- a/src/components/sidebar/hit_dice.rs +++ b/src/components/sidebar/hit_dice.rs @@ -1,6 +1,8 @@ use crate::prelude::*; use leptos::prelude::*; +/// Hit dice: a die-kind selector (d4 … d20) and one clickable die per +/// character level; clicking toggles spent/unspent. #[component] pub fn HitDice () -> impl IntoView { let state = expect_context::(); @@ -8,6 +10,8 @@ pub fn HitDice () -> impl IntoView { let hit_dice = player.hit_dice(); let level = player.level(); + // Toggles die `slot_id` spent/unspent; clicking the last spent die + // frees it again. let process_die_click = move |slot_id: u8| { hit_dice.update(|d| { if slot_id < d.used { diff --git a/src/components/sidebar/hit_points.rs b/src/components/sidebar/hit_points.rs index 7e23970..1d3df35 100644 --- a/src/components/sidebar/hit_points.rs +++ b/src/components/sidebar/hit_points.rs @@ -1,6 +1,8 @@ use crate::prelude::*; use leptos::prelude::*; +/// Current/max HP with a fill bar, plus temp HP with its own bar. +/// All three numbers are editable `type="number"` inputs. #[component] pub fn HitPoints () -> impl IntoView { let state = expect_context::(); @@ -10,6 +12,8 @@ pub fn HitPoints () -> impl IntoView { let max_hp = player.max_hp(); let temp_hp = player.temp_hp(); + // All three inputs parse via `utils::adjust_checked`; invalid text + // restores the previous value. let adjust_hp = move |ev: Targeted| { utils::adjust_checked(ev, hp); }; diff --git a/src/components/sidebar/image.rs b/src/components/sidebar/image.rs index 98aff86..ce68e29 100644 --- a/src/components/sidebar/image.rs +++ b/src/components/sidebar/image.rs @@ -1,6 +1,7 @@ use crate::prelude::*; use leptos::prelude::*; +/// Character portrait; hidden while no image URL is set. #[component] pub fn Image () -> impl IntoView { let state = expect_context::(); diff --git a/src/components/sidebar/names.rs b/src/components/sidebar/names.rs index 4df6439..69df6b2 100644 --- a/src/components/sidebar/names.rs +++ b/src/components/sidebar/names.rs @@ -1,6 +1,8 @@ use crate::prelude::*; use leptos::prelude::*; +/// First name, alias (in «»), last name — three text inputs bound to the +/// player's name store. #[component] pub fn Names () -> impl IntoView { let state = expect_context::(); diff --git a/src/components/sidebar/spell_slots.rs b/src/components/sidebar/spell_slots.rs index 0c2994a..4971c1e 100644 --- a/src/components/sidebar/spell_slots.rs +++ b/src/components/sidebar/spell_slots.rs @@ -1,6 +1,8 @@ use crate::prelude::*; use leptos::prelude::*; +/// Spell slots for all nine levels: a row of clickable pips per level. +/// Clicking toggles a slot used/unused. #[component] pub fn SpellSlots () -> impl IntoView { let state = expect_context::(); @@ -8,6 +10,7 @@ pub fn SpellSlots () -> impl IntoView { let slots = player.spell_slots(); + // Toggles pip `slot_id` of `level` used/unused. let process_spell_slot_click = move |level: usize, slot_id: u8| { slots.update(|s| { let l = &mut s.0[level]; diff --git a/src/entities/mock.rs b/src/entities/mock.rs index a624e44..36f80fb 100644 --- a/src/entities/mock.rs +++ b/src/entities/mock.rs @@ -1,7 +1,14 @@ +//! Hardcoded demo data. +//! +//! `Dashboard::mock()` is the app's only state source until persistence +//! lands (ROADMAP phase 1). use crate::prelude::*; use super::*; impl Dashboard { +/// Builds a fully populated demo dashboard: a level-8 character, two +/// inventories, quests, notes and contacts. Used at startup and as the +/// fallback state. pub fn mock () -> Self { let (p_inv, c_inv) = generate_mock_inventory(); @@ -47,6 +54,8 @@ impl Dashboard { +/// Demo inventories: 10 personal items and 10 common items, each with a +/// weight, description and tags. fn generate_mock_inventory() -> (Inventory, Inventory) { let a = vec![ InventoryEntry { id: 1, quantity: 1, item: InventoryItem { name: "Vorpal Sword".to_string(), weight: 400, description: Some("A blade that severs heads with a single swing.".to_string()), tags: HashMap::from([ ("type".to_string(), "weapon".to_string()), ("rarity".to_string(), "legendary".to_string()), ]), }, }, @@ -79,6 +88,8 @@ fn generate_mock_inventory() -> (Inventory, Inventory) { +/// Demo notes: one per session, with in-game dates, real timestamps and +/// tags. fn generate_mock_notebook () -> NoteBook { NoteBook(vec![ Note { session: 1, content: "Met Elminster in Waterdeep; hinted at Netherese ruin.".to_string(), date_ingame: NaiveDate::from_ymd_opt(1481, 4, 15).unwrap(), date_real: NaiveDateTime::parse_from_str("2025-09-01 14:30:00", "%Y-%m-%d %H:%M:%S").unwrap(), tags: HashSet::from_iter(vec!["Elminster".to_string(), "Waterdeep".to_string()]) }, @@ -96,6 +107,7 @@ fn generate_mock_notebook () -> NoteBook { ]) } +/// Demo quests with givers, locations, dates and rewards. fn generate_mock_questbook () -> QuestBook { QuestBook(vec![ Quest { title: "Slay the Bandit Chief".to_string(), location_taken: Some("Waterdeep".to_string()), location_task: Some("Phandalin".to_string()), date_taken: Some(NaiveDate::from_ymd_opt(1481, 4, 15).unwrap()), date_by: Some(NaiveDate::from_ymd_opt(1481, 5, 15).unwrap()), description: Some("Kill the bandit leader.".to_string()), from: Some(Name { first: "Dagult".to_string(), last: "Neverember".to_string(), alias: "".to_string(), ..Default::default() }), reward: Some(Balance { platinum: 0, gold: 100, electrum: 0, silver: 0, copper: 0 }) }, @@ -111,6 +123,8 @@ fn generate_mock_questbook () -> QuestBook { ]) } +/// Demo contacts: mostly NPCs met by the party, plus a few merchants, +/// living legends and the dead. fn generate_mock_contact_book () -> ContactBook { let mut contacts = HashMap::new(); // 40 Characters Met by Party (80%) @@ -172,6 +186,7 @@ fn generate_mock_contact_book () -> ContactBook { } +/// Demo prepared spells. Currently an empty list — `Spell` is a stub. fn generate_prepared_spells_list () -> PreparedSpells { use PreparedSpell::*; let list = vec![ diff --git a/src/entities/mod.rs b/src/entities/mod.rs index b498e4b..e340131 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,3 +1,16 @@ +//! The domain model of the campaign dashboard. +//! +//! Every type here is a plain data struct that derives +//! [`reactive_stores::Store`], which makes each field independently +//! reactive: components subscribe to single fields and re-render only when +//! those change. The root type is [`Dashboard`], wrapped in a +//! `Store` and provided to the whole app through Leptos context +//! (see `crate::prelude::Context`). +//! +//! The model is deliberately "wide": it covers the campaign header, the +//! player character, the party's common stash, quests, notes, contacts and +//! settings. All of it currently comes from hardcoded mock data (the `mock` +//! module); nothing is persisted yet (see ROADMAP phase 1). use std::fmt::Display; use chrono::{ NaiveDate, NaiveDateTime }; @@ -7,6 +20,10 @@ use crate::prelude::*; mod mock; +/// A single item as it appears in the bundled 5e.tools JSON files. +/// +/// `rest` keeps every other JSON field verbatim, so the raw source data +/// survives lookup (used by future item detail views). #[derive(Clone, Debug, Store)] pub struct BookItem { pub name: String, @@ -16,6 +33,8 @@ pub struct BookItem { } impl BookItem { +/// Parses one 5e.tools JSON object into a `BookItem`. +/// Returns `None` when the object has no string `name` or is not an object. pub fn from_json (t: Value) -> Option { let Value::String(name) = t["name"].clone() else { return None }; let weight = match t["weight"].clone() { @@ -36,9 +55,17 @@ impl BookItem { } } +/// Name → `BookItem` lookup, built once from the bundled JSON (see `statics`). #[derive(Clone, Debug, Store)] pub struct ItemReferenceMap (pub HashMap); +/// The root application state. +/// +/// One `Dashboard` holds everything the UI edits: campaign header data +/// (name, image, session number, in-game date), the player character, the +/// party's common inventory, quests, notes, contacts and settings. The +/// whole tree derives `Store`, so every nested field is independently +/// reactive. `Dashboard::mock()` provides the initial state. #[derive(Clone, Debug, Store)] pub struct Dashboard { pub campaign: String, @@ -53,6 +80,9 @@ pub struct Dashboard { pub settings: Settings } +/// The player character: identity, class, level, XP, hit points, money, +/// spell slots, inventory, attunements, prepared spells, hit dice and +/// death saves. This is the heart of the character sheet. #[derive(Clone, Debug, Store)] pub struct PlayerData { pub name: Name, @@ -74,12 +104,15 @@ pub struct PlayerData { } +/// The party's shared stash: money and items usable by everyone. #[derive(Clone, Debug, Store)] pub struct CommonData { pub balance: Balance, pub inventory: Inventory, } +/// Currency counts per denomination, platinum through copper. +/// Edited with `Numput` on the inventories page. #[derive(Clone, Debug, Store)] pub struct Balance { pub platinum: u32, @@ -89,12 +122,17 @@ pub struct Balance { pub copper: u32, } +/// A named inventory (personal or common): a list of entries. +/// +/// `#[store(key)]` gives every entry a stable reactive key (its `id`), +/// which keeps `For`/`ForEnumerate` updates cheap. #[derive(Clone, Debug, Store)] pub struct Inventory { #[store(key: u64 = |row| row.id)] pub rows: Vec } +/// One inventory row: a quantity of a single item. #[derive(Clone, Debug, Store, Default)] pub struct InventoryEntry { pub id: u64, @@ -103,6 +141,7 @@ pub struct InventoryEntry { } impl InventoryEntry { +/// Returns a stable key string for this entry (quantity + item name). pub fn key (&self) -> String { format!("{}-{}", self.quantity, self.item.name) } @@ -114,6 +153,8 @@ impl InventoryEntry { // Book (Item), // } +/// An item inside an inventory: name, weight (basis points), optional +/// description, and free-form tags (e.g. `type`, `rarity`). #[derive(Clone, Debug, Store, Default)] pub struct InventoryItem { pub name: String, @@ -122,9 +163,12 @@ pub struct InventoryItem { pub tags: HashMap, } +/// The party's quest list. No UI yet — see ROADMAP phase 4. #[derive(Clone, Debug, Store)] pub struct QuestBook (pub Vec); +/// One quest: title, locations, dates, description, giver and reward. +/// Note: there is no completion state yet (ROADMAP phase 4 adds one). #[derive(Clone, Debug, Store)] pub struct Quest { pub title: String, @@ -137,9 +181,12 @@ pub struct Quest { pub reward: Option, } +/// The party's session notes. No UI yet — see ROADMAP phase 5. #[derive(Clone, Debug, Store)] pub struct NoteBook (pub Vec); +/// One note: content, in-game date, real-world timestamp, session number +/// and tags. #[derive(Clone, Debug, Store)] pub struct Note { pub content: String, @@ -149,35 +196,43 @@ pub struct Note { pub tags: HashSet, } +/// Spell slots for the nine spell levels (index 0 = 1st level). #[derive(Clone, Debug, Store)] pub struct SpellSlots (pub [SpellSlotLevel; 9]); +/// Used/total slots for one spell level. #[derive(Clone, Debug, Store)] pub struct SpellSlotLevel { pub used: u8, pub total: u8, } +/// Three attunement slots (D&D rules allow three attuned items). #[derive(Clone, Debug, Store)] pub struct Attunements (pub [Option; 3]); +/// The spell list the character has prepared (or is learning). #[derive(Clone, Debug, Store)] pub struct PreparedSpells (pub Vec); +/// One prepared-spell slot: vacant or holding a spell. #[derive(Clone, Debug, Store)] pub enum PreparedSpell { Vacant, Occupied (Spell) } +/// A spell. Only the name exists so far; the rest is a `@TODO` +/// (see ROADMAP phase 2). // @TODO #[derive(Clone, Debug, Store)] pub struct Spell { pub name: String } +/// Hit dice: how many of the current die kind are already spent. #[derive(Clone, Debug, Store)] pub struct HitDice { pub used: u8, @@ -185,6 +240,8 @@ pub struct HitDice { } +/// Death-saving-throw counters: three failures kill, three successes +/// stabilize. #[derive(Clone, Debug, Store)] pub struct DeathSaveThrows { pub failed: u8, @@ -192,15 +249,18 @@ pub struct DeathSaveThrows { } impl DeathSaveThrows { +/// Clears both counters. Bound to the ⟳ button in the sidebar. pub fn reset (&mut self) { self.failed = 0; self.succeeded = 0; } } +/// Known NPCs keyed by name. No UI yet — see ROADMAP phase 6. #[derive(Clone, Debug, Store)] pub struct ContactBook (pub HashMap); +/// One NPC contact: name, optional image, status, location and notes. #[derive(Clone, Debug, Store)] pub struct Contact { pub name: Name, @@ -210,6 +270,7 @@ pub struct Contact { pub notes: Option, } +/// Life status of a contact, used for filtering. #[derive(Clone, Debug, Store)] pub enum ContactStatus { Unknown, @@ -218,6 +279,7 @@ pub enum ContactStatus { Alive, } +/// A person's name: first, last, and an optional alias (nickname). #[derive(Clone, Debug, Store, Default)] pub struct Name { pub first: String, @@ -226,26 +288,31 @@ pub struct Name { } impl Name { +/// True when an alias is set. pub fn display_alias (&self) -> bool { !self.alias.is_empty() } +/// True when a last name is set. pub fn display_last (&self) -> bool { !self.last.is_empty() } } +/// App settings. Empty today; will carry e.g. a persisted-format version. #[derive(Clone, Debug, Store, Default)] pub struct Settings { } +/// Which side of the party an inventory belongs to. #[derive(Clone, Copy, Debug, Store)] pub enum InventoryKind { Personal, Common } +/// Russian display name ("Личный" / "Общий"). impl Display for InventoryKind { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", match self { @@ -256,6 +323,7 @@ impl Display for InventoryKind { } impl InventoryKind { +/// The other inventory (for the move-between button). pub fn dst (&self) -> Self { match self { Self::Common => Self::Personal, @@ -263,6 +331,7 @@ impl InventoryKind { } } +/// CSS classes for the inventory section (personal vs common styling). pub fn class (&self) -> &'static str { match self { Self::Common => "inventory inventory-common", diff --git a/src/lib.rs b/src/lib.rs index fd0a6c8..7ee24e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,8 @@ +//! aex — a D&D 5e character sheet and campaign dashboard. +//! +//! Leptos 0.8 app with actix-web SSR and client-side hydration. The crate +//! root declares the modules and the WASM entry point; `main.rs` holds the +//! SSR server, `app.rs` the component tree. #![feature(type_alias_impl_trait)] pub mod app; @@ -21,6 +26,8 @@ mod components; #[allow(unused_imports)] use prelude::*; +/// WASM entry point: hydrates `App` into the server-rendered DOM. +/// Only compiled with the `hydrate` feature. #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() { diff --git a/src/main.rs b/src/main.rs index 9038f32..4eb630a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,9 @@ +/// SSR entry point: starts the actix-web server that renders the app. +/// +/// Serves the WASM/CSS bundle from `/pkg`, static assets from `/assets`, +/// and renders every route through `leptos_routes`. Only compiled with +/// the `ssr` feature. #[cfg(feature = "ssr")] #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -58,6 +63,7 @@ async fn main() -> std::io::Result<()> { .await } +/// Serves `/favicon.ico` from the site root. #[cfg(feature = "ssr")] #[actix_web::get("favicon.ico")] async fn favicon( @@ -70,6 +76,8 @@ async fn favicon( ))?) } +/// No-op `main` for builds without `ssr` or `csr`: the WASM binary is +/// hydrated from `lib.rs` instead. #[cfg(not(any(feature = "ssr", feature = "csr")))] pub fn main() { // no client-side main function @@ -78,6 +86,8 @@ pub fn main() { // see optional feature `csr` instead } +/// Client-only `main` for `trunk serve --features csr`. +/// Not the normal path — prefer `cargo leptos serve`. #[cfg(all(not(feature = "ssr"), feature = "csr"))] pub fn main() { // a client-side main function is required for using `trunk serve` diff --git a/src/prelude.rs b/src/prelude.rs index 9bf1fbf..e99d31b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,3 +1,7 @@ +//! Crate-wide re-exports. +//! +//! Components and entities use `use crate::prelude::*` to get the common +//! imports: collections, logging, error aliases, `Store`, entities, utils. #![allow(unused_imports)] #![allow(dead_code)] pub use std::collections::{ HashMap, HashSet }; @@ -16,6 +20,12 @@ pub (crate) use crate::{ components::{ self, numput::{ Numput, NumputChange } }, }; +/// Crate-wide error type. Alias of `anyhow::Error`. pub type Error = anyhow::Error; +/// Crate-wide result type. Alias of `anyhow::Result`. pub type Result = anyhow::Result; +/// The reactive application state: a `Store` wrapping [`crate::entities::Dashboard`]. +/// +/// Created once in `App` and injected through Leptos context. Components +/// read it with `expect_context::()` and grab reactive field handles. pub type Context = Store; diff --git a/src/state/mod.rs b/src/state/mod.rs index 3616db2..96c0a33 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1 +1,6 @@ +//! App state. +//! +//! The whole dashboard lives in one `reactive_stores::Store` +//! (alias `Context`), created in `App` and shared through Leptos context. + use crate::prelude::*; diff --git a/src/statics/mod.rs b/src/statics/mod.rs index da21bbe..27ce679 100644 --- a/src/statics/mod.rs +++ b/src/statics/mod.rs @@ -1,8 +1,18 @@ +//! Bundled static game data. +//! +//! `ITEMS_REFS` loads the 5e.tools-format JSON files (`data/*.json`, +//! roughly 5k items) once at first use via `lazy_static` and indexes them +//! by name. `ITEMS_NAMES` is the flat name list used for fuzzy search. use crate::prelude::*; use lazy_static::lazy_static; lazy_static! { +/// All known items, indexed by name, from the bundled 5e.tools JSON files. +/// +/// Built once at startup: entries with the same name are merged (a missing +/// weight fills in, `rest` fields concatenate). Note `foundry.json` is +/// currently loaded twice — see `utils::read_items`. pub static ref ITEMS_REFS: ItemReferenceMap = { let mut map = HashMap::with_capacity(5_000); @@ -17,6 +27,8 @@ lazy_static! { ItemReferenceMap(HashMap::from_iter(map.into_iter())) }; +/// Flat list of item names; the feed for the fuzzy matcher +/// (`ItemSearchField`). pub static ref ITEMS_NAMES: Vec<&'static String> = { ITEMS_REFS.0.keys().collect() }; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9b8cb44..465b469 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,5 @@ +//! Small shared helpers: item parsing, checked input parsing, percentage +//! math, and the `BasisPoints` display trait. use crate::prelude::*; use std::str::FromStr; use leptos::ev::{ Event, Targeted }; @@ -5,6 +7,11 @@ use leptos::web_sys::HtmlInputElement; use leptos::reactive::traits::{ Get, Set }; +/// Parses a 5e.tools-format JSON array of items into `map`, keyed by name. +/// +/// Skips entries without a string `name` or without an object body. +/// Duplicate names merge: the later entry's weight fills a missing one and +/// its `rest` fields are appended. Returns the number of new names added. pub fn read_items (contents: &str, map: &mut HashMap) -> usize { let Ok(json): std::result::Result = serde_json::from_str(&contents) else { error!("Can't read Value from contents"); return 0 }; let Value::Array(list) = json else { error!("Value isn't Array, skipping"); return 0 }; @@ -24,6 +31,10 @@ pub fn read_items (contents: &str, map: &mut HashMap) -> usize map.len() - start } +/// Applies a text input's value to a reactive store with checked parsing. +/// +/// On a successful parse, sets the store to the value; otherwise restores +/// the input to the store's current value. Used for `type="number"` inputs. pub fn adjust_checked (ev: Targeted, store: U) -> () where U: Set + Get, @@ -39,6 +50,8 @@ pub fn adjust_checked (ev: Targeted, store: U) - } } +/// Percent of `cur` relative to `max`; callers clamp to 100. Used for +/// HP-bar widths. pub fn filled_pc (cur: T, max: T) -> f32 where T: Into { let cur: f32 = cur.into(); let max: f32 = max.into(); @@ -46,10 +59,13 @@ pub fn filled_pc (cur: T, max: T) -> f32 where T: Into { } +/// Formats an integer stored in basis points (hundredths) as a decimal +/// string, e.g. `400` → `"4.00"`. pub trait BasisPoints { fn to_decimal_string (&self) -> String; } +/// Weights are stored as `u32` basis points: 100 = 1.0. impl BasisPoints for u32 { fn to_decimal_string (&self) -> String { let int = self / 100;