csr version imported

This commit is contained in:
YK 2025-09-11 10:27:57 +03:00
parent 2a1fb81567
commit b09dcb5fc1
19 changed files with 122928 additions and 5 deletions

View File

@ -15,7 +15,20 @@ leptos = { version = "0.8.2", features = ["nightly"] }
leptos_meta = { version = "0.8.2" }
leptos_actix = { version = "0.8.2", optional = true }
leptos_router = { version = "0.8.2", features = ["nightly"] }
wasm-bindgen = "=0.2.100"
wasm-bindgen = "=0.2.101"
anyhow = "1.0.99"
thiserror = "2.0.16"
itertools = "0.14.0"
dotenvy = "0.15.7"
pretty_env_logger = "0.5.0"
lazy_static = "1.5.0"
leptos-use = "0.16.2"
log = "0.4.28"
nucleo-matcher = "0.3.1"
reactive_stores = "0.2.5"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
chrono = { version = "0.4.42", features = ["serde"] }
[features]
csr = ["leptos/csr"]

4265
data/base.json Normal file

File diff suppressed because it is too large Load Diff

11871
data/fluff.json Normal file

File diff suppressed because it is too large Load Diff

23593
data/foundry.json Normal file

File diff suppressed because it is too large Load Diff

74409
data/items.json Normal file

File diff suppressed because it is too large Load Diff

7088
data/magic.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,8 @@ use leptos_router::{
StaticSegment, WildcardSegment,
};
use crate::components::Dash;
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
@ -22,7 +24,7 @@ pub fn App() -> impl IntoView {
<Router>
<main>
<Routes fallback=move || "Not found.">
<Route path=StaticSegment("") view=HomePage/>
<Route path=StaticSegment("") view=Dash/>
<Route path=WildcardSegment("any") view=NotFound/>
</Routes>
</main>

17
src/components/header.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::{ entities::DashboardStoreFields, prelude::* };
use leptos::prelude::*;
#[component]
pub fn Header () -> impl IntoView {
let state = expect_context::<Context>();
let campaign = state.campaign();
let date = state.date();
view! {
<header>
<h1>Aboba v{env!("CARGO_PKG_VERSION")} by mk</h1>
<h2>{campaign.get_untracked()}</h2>
<h3>{move || date.get().to_string()}</h3>
</header>
}
}

73
src/components/mod.rs Normal file
View File

@ -0,0 +1,73 @@
use leptos::prelude::*;
use nucleo_matcher::{pattern::{AtomKind, CaseMatching, Pattern, Normalization}, Config, Matcher};
use crate::prelude::*;
mod header;
#[component]
pub fn Dash () -> impl IntoView {
provide_context(Store::new(Dashboard::mock()));
use header::Header;
view! {
<div class="wrapper">
<Header />
<main></main>
</div>
}
}
#[component]
pub fn ItemSearchField () -> impl IntoView {
let (query, set_query) = signal("".to_owned());
let (match_list, set_match_list) = signal(vec![]);
Effect::new(move |_| {
let q = query.get();
if !q.trim().is_empty() {
let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
set_match_list.set(Pattern::parse(&q, CaseMatching::Ignore, Normalization::Smart).match_list(ITEMS_NAMES.iter(), &mut matcher))
} else {
set_match_list.set(vec![]);
}
});
view! {
<input
type="text"
on:input:target=move |ev| {
set_query.set(ev.target().value());
}
/>
<Show when=move || {!query.get().trim().is_empty()}>
<p>{query.get()}: {match_list.get().len()}" results found"</p>
</Show>
<ul class="results">
<For
each=move || match_list.get().into_iter().map(|(name, _score)| ITEMS_REFS.0.get(*name)).flatten()
key=|entry| entry.name.clone()
let(item)
>
<ItemSearchCard item=item />
</For>
</ul>
}
}
#[component]
pub fn ItemSearchCard <'a> (item: &'a Item) -> impl IntoView {
let card_class = format!("item-search-card {}", item.rarity.as_ref().unwrap_or(&String::new()));
view! {
<div class=card_class>
<div>{item.name.clone()}</div>
<div>{item.weight.clone()}</div>
<div>{item.rarity.clone()}</div>
<button>+</button>
</div>
}
}

368
src/entities/mock.rs Normal file
View File

@ -0,0 +1,368 @@
use crate::prelude::*;
use super::*;
impl Dashboard {
pub fn mock () -> Self {
Dashboard {
campaign: "В Поисках Дмитрия Шардалина".to_owned(),
date: NaiveDate::from_ymd(1488, 3, 19),
player: PlayerData {
name: "Ней Гигга Визард".to_owned(),
image: String::new(),
level: 8,
xp: 3231,
temp_hp: 12,
hp: 1,
max_hp: 61,
balance: Balance { platinum: 12, gold: 200, electrum: 1, silver: 101, copper: 129021021 },
spell_slots: SpellSlots(core::array::from_fn(|i| SpellSlotLevel { used: 1, total: 9 - i as u8 })),
attunements: Attunements([const { None }; 3]),
hit_dice: HitDice { used: 1, total: 7 },
death_save_throws: DeathSaveThrows { failed: 1, succeeded: 3 },
prepared: PreparedSpells(vec![Spell { name: "Fire Bolt".to_owned() }]),
inventory: generate_mock_inventory(),
},
common: CommonData {
balance: Balance { platinum: 1312, gold: 3211200, electrum: 231, silver: 1, copper: 21021 },
inventory: generate_mock_inventory(),
},
quest_book: 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("Lord Neverember".to_string()), reward: Some(Balance { platinum: 0, gold: 100, electrum: 0, silver: 0, copper: 0 }) },
Quest { title: "Find the Lost Artifact".to_string(), location_taken: Some("Baldur's Gate".to_string()), location_task: None, date_taken: Some(NaiveDate::from_ymd_opt(1481, 6, 1).unwrap()), date_by: None, description: Some("Seek a relic in ruins.".to_string()), from: Some("Elminster".to_string()), reward: Some(Balance { platinum: 10, gold: 50, electrum: 0, silver: 0, copper: 0 }) },
Quest { title: "Escort the Caravan".to_string(), location_taken: Some("Neverwinter".to_string()), location_task: Some("Luskan".to_string()), date_taken: None, date_by: Some(NaiveDate::from_ymd_opt(1481, 7, 30).unwrap()), description: Some("Protect traders.".to_string()), from: None, reward: Some(Balance { platinum: 0, gold: 20, electrum: 0, silver: 50, copper: 0 }) },
Quest { title: "Clear the Goblin Den".to_string(), location_taken: Some("Daggerford".to_string()), location_task: Some("Cragmaw Hideout".to_string()), date_taken: Some(NaiveDate::from_ymd_opt(1481, 3, 10).unwrap()), date_by: Some(NaiveDate::from_ymd_opt(1481, 4, 10).unwrap()), description: None, from: Some("Local Militia".to_string()), reward: Some(Balance { platinum: 0, gold: 30, electrum: 0, silver: 0, copper: 100 }) },
Quest { title: "Rescue the Noble".to_string(), location_taken: None, location_task: Some("Underdark".to_string()), date_taken: Some(NaiveDate::from_ymd_opt(1481, 5, 20).unwrap()), date_by: None, description: Some("Save a kidnapped noble.".to_string()), from: Some("Silverhand Family".to_string()), reward: Some(Balance { platinum: 5, gold: 200, electrum: 0, silver: 0, copper: 0 }) },
Quest { title: "Steal the Zhentarim Plans".to_string(), location_taken: Some("Zhentil Keep".to_string()), location_task: Some("Zhentil Keep".to_string()), date_taken: None, date_by: Some(NaiveDate::from_ymd_opt(1481, 8, 15).unwrap()), description: None, from: Some("Harpers".to_string()), reward: Some(Balance { platinum: 0, gold: 75, electrum: 0, silver: 0, copper: 0 }) },
Quest { title: "Hunt the Dire Wolf".to_string(), location_taken: Some("Silverymoon".to_string()), location_task: Some("Evermoors".to_string()), date_taken: Some(NaiveDate::from_ymd_opt(1481, 2, 5).unwrap()), date_by: None, description: Some("Track and kill a beast.".to_string()), from: None, reward: Some(Balance { platinum: 0, gold: 15, electrum: 0, silver: 20, copper: 50 }) },
Quest { title: "Recover the Spellbook".to_string(), location_taken: Some("Candlekeep".to_string()), location_task: None, date_taken: Some(NaiveDate::from_ymd_opt(1481, 1, 10).unwrap()), date_by: Some(NaiveDate::from_ymd_opt(1481, 2, 10).unwrap()), description: Some("Find a stolen tome.".to_string()), from: Some("Candlekeep Scholars".to_string()), reward: None },
Quest { title: "Guard the Temple".to_string(), location_taken: Some("Suzail".to_string()), location_task: Some("Temple of Lathander".to_string()), date_taken: None, date_by: None, description: Some("Protect the shrine.".to_string()), from: Some("Clerics of Lathander".to_string()), reward: Some(Balance { platinum: 0, gold: 50, electrum: 0, silver: 0, copper: 0 }) },
Quest { title: "Explore the Ruins".to_string(), location_taken: Some("Cormyr".to_string()), location_task: Some("Netherese Ruins".to_string()), date_taken: Some(NaiveDate::from_ymd_opt(1481, 9, 1).unwrap()), date_by: Some(NaiveDate::from_ymd_opt(1481, 10, 1).unwrap()), description: None, from: None, reward: None },
]),
notes: NoteBook(
vec![
Note("Met Elminster in Waterdeep; he hinted at a Netherese ruin nearby.".to_string()),
Note("Avoid Zhentarim agents in Baldur's Gate; they're watching the tavern.".to_string()),
Note("Phandalin: Redbrand thugs extorting locals, hideout in manor basement.".to_string()),
Note("Neverwinter's Lord Neverember offering 100gp for bandit leader's head.".to_string()),
Note("Found a strange rune in Underdark; ask Candlekeep scholars about it.".to_string()),
Note("Luskan pirates smuggling in the docks; check crates for clues.".to_string()),
Note("Silverymoon's wards flicker at midnight; possible arcane sabotage?".to_string()),
Note("Daggerford militia needs help with goblin raids from Cragmaw.".to_string()),
Note("Temple of Lathander in Suzail requests guards for festival.".to_string()),
Note("Heard rumors of a dragon in Evermoors; prepare fire resistance.".to_string()),
Note("Harpers want Zhentarim plans stolen from Zhentil Keep by next tenday.".to_string()),
Note("Keep an eye on that shady merchant in Cormyr; might be a doppelganger.".to_string()),
]),
people: generate_mock_contact_book(),
}
}
}
fn generate_mock_inventory() -> Inventory {
let items = vec![
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Vorpal Sword".to_string(),
weight: Some(4.0),
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()),
]),
}),
},
InventoryEntry {
quantity: 3,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Potion of Healing".to_string(),
weight: Some(0.5),
description: Some("A vial of red liquid that restores vitality.".to_string()),
tags: HashMap::from([
("type".to_string(), "potion".to_string()),
("rarity".to_string(), "common".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Cloak of Elvenkind".to_string(),
weight: Some(1.0),
description: Some("A cloak that blends into shadows.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "uncommon".to_string()),
]),
}),
},
InventoryEntry {
quantity: 2,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Bag of Holding".to_string(),
weight: Some(15.0),
description: Some("A small bag with vast internal space.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "uncommon".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Mithril Chainmail".to_string(),
weight: Some(20.0),
description: Some("Lightweight armor that gleams like silver.".to_string()),
tags: HashMap::from([
("type".to_string(), "armor".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Wand of Fireballs".to_string(),
weight: Some(0.2),
description: Some("A wand that unleashes fiery explosions.".to_string()),
tags: HashMap::from([
("type".to_string(), "wand".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Ring of Invisibility".to_string(),
weight: Some(0.1),
description: Some("Grants the wearer invisibility.".to_string()),
tags: HashMap::from([
("type".to_string(), "ring".to_string()),
("rarity".to_string(), "legendary".to_string()),
]),
}),
},
InventoryEntry {
quantity: 2,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Boots of Speed".to_string(),
weight: Some(1.5),
description: Some("Boots that enhance the wearer's speed.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Dagger of Venom".to_string(),
weight: Some(1.0),
description: Some("A dagger coated with deadly poison.".to_string()),
tags: HashMap::from([
("type".to_string(), "weapon".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Staff of Power".to_string(),
weight: Some(5.0),
description: Some("A staff pulsing with arcane might.".to_string()),
tags: HashMap::from([
("type".to_string(), "staff".to_string()),
("rarity".to_string(), "very rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 4,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Torch".to_string(),
weight: Some(1.0),
description: Some("A simple torch for illumination.".to_string()),
tags: HashMap::from([
("type".to_string(), "tool".to_string()),
("rarity".to_string(), "common".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Amulet of Health".to_string(),
weight: Some(0.3),
description: Some("Boosts the wearer's vitality.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Flame Tongue".to_string(),
weight: Some(3.0),
description: Some("A sword that ignites on command.".to_string()),
tags: HashMap::from([
("type".to_string(), "weapon".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 5,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Arrow of Slaying".to_string(),
weight: Some(0.1),
description: Some("An arrow designed to kill a specific foe.".to_string()),
tags: HashMap::from([
("type".to_string(), "ammunition".to_string()),
("rarity".to_string(), "very rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Cloak of Displacement".to_string(),
weight: Some(1.0),
description: Some("Makes the wearer hard to hit.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Helm of Teleportation".to_string(),
weight: Some(3.0),
description: Some("Allows teleportation to known locations.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Crystal Ball".to_string(),
weight: Some(7.0),
description: Some("A magical orb for scrying.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "very rare".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Gauntlets of Ogre Power".to_string(),
weight: Some(2.0),
description: Some("Grants the strength of an ogre.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "uncommon".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Robe of the Archmagi".to_string(),
weight: Some(1.0),
description: Some("Enhances a wizard's spellcasting.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "legendary".to_string()),
]),
}),
},
InventoryEntry {
quantity: 1,
item: InventoryItem::Custom(CustomInventoryItem {
name: "Horn of Blasting".to_string(),
weight: Some(2.0),
description: Some("Emits a destructive sonic blast.".to_string()),
tags: HashMap::from([
("type".to_string(), "wondrous".to_string()),
("rarity".to_string(), "rare".to_string()),
]),
}),
},
];
Inventory(items)
}
fn generate_mock_contact_book() -> ContactBook {
let mut contacts = HashMap::new();
// 40 Characters Met by Party (80%)
contacts.insert("Elminster".to_string(), Contact { name: "Elminster".to_string(), image: None, alias: Some("The Sage of Shadowdale".to_string()), status: Some(ContactStatus::Alive), location: Some("Shadowdale".to_string()), notes: Some("Wise but cryptic wizard.".to_string()) });
contacts.insert("Drizzt Do'Urden".to_string(), Contact { name: "Drizzt Do'Urden".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Icewind Dale".to_string()), notes: Some("Renegade drow ranger.".to_string()) });
contacts.insert("Lord Neverember".to_string(), Contact { name: "Lord Neverember".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Neverwinter".to_string()), notes: Some("Ruler seeking bandit leader.".to_string()) });
contacts.insert("Minsc".to_string(), Contact { name: "Minsc".to_string(), image: None, alias: Some("Rascal of Rashemen".to_string()), status: Some(ContactStatus::Alive), location: Some("Baldur's Gate".to_string()), notes: Some("Loud warrior with hamster.".to_string()) });
contacts.insert("Jarlaxle".to_string(), Contact { name: "Jarlaxle".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Luskan".to_string()), notes: Some("Sly drow mercenary leader.".to_string()) });
contacts.insert("Volo".to_string(), Contact { name: "Volo".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Waterdeep".to_string()), notes: Some("Exaggerates his travel tales.".to_string()) });
contacts.insert("Laeral Silverhand".to_string(), Contact { name: "Laeral Silverhand".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Waterdeep".to_string()), notes: Some("Open Lord, powerful mage.".to_string()) });
contacts.insert("Hammond Ardeep".to_string(), Contact { name: "Hammond Ardeep".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Daggerford".to_string()), notes: Some("Militia leader vs. goblins.".to_string()) });
contacts.insert("Sister Garaele".to_string(), Contact { name: "Sister Garaele".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Phandalin".to_string()), notes: Some("Harper agent in temple.".to_string()) });
contacts.insert("Gundren Rockseeker".to_string(), Contact { name: "Gundren Rockseeker".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Phandalin".to_string()), notes: Some("Dwarf seeking lost mine.".to_string()) });
contacts.insert("Sildar Hallwinter".to_string(), Contact { name: "Sildar Hallwinter".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Phandalin".to_string()), notes: Some("Lord's Alliance knight.".to_string()) });
contacts.insert("Qelline Alderleaf".to_string(), Contact { name: "Qelline Alderleaf".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Phandalin".to_string()), notes: Some("Halfling farmer, kind.".to_string()) });
contacts.insert("Toblen Stonehill".to_string(), Contact { name: "Toblen Stonehill".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Phandalin".to_string()), notes: Some("Innkeeper, knows rumors.".to_string()) });
contacts.insert("Darathra Shendrel".to_string(), Contact { name: "Darathra Shendrel".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Triboar".to_string()), notes: Some("Lord Protector, fair.".to_string()) });
contacts.insert("Ziraj the Hunter".to_string(), Contact { name: "Ziraj the Hunter".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Neverwinter Wood".to_string()), notes: Some("Tracks orcs.".to_string()) });
contacts.insert("Eldrin Thalindra".to_string(), Contact { name: "Eldrin Thalindra".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Silverymoon".to_string()), notes: Some("Arcane ward keeper.".to_string()) });
contacts.insert("Randal Morn".to_string(), Contact { name: "Randal Morn".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Daggerdale".to_string()), notes: Some("Fights Zhentarim.".to_string()) });
contacts.insert("Khelben Blackstaff".to_string(), Contact { name: "Khelben Blackstaff".to_string(), image: None, alias: Some("The Blackstaff".to_string()), status: Some(ContactStatus::Dead), location: Some("Waterdeep".to_string()), notes: Some("Former archmage, deceased.".to_string()) });
contacts.insert("Mirt the Moneylender".to_string(), Contact { name: "Mirt the Moneylender".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Waterdeep".to_string()), notes: Some("Harper ally, wealthy.".to_string()) });
contacts.insert("Durnan".to_string(), Contact { name: "Durnan".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Waterdeep".to_string()), notes: Some("Yawning Portal owner.".to_string()) });
contacts.insert("Fzoul Chembryl".to_string(), Contact { name: "Fzoul Chembryl".to_string(), image: None, alias: None, status: Some(ContactStatus::Dead), location: Some("Zhentil Keep".to_string()), notes: Some("Former Zhentarim leader.".to_string()) });
contacts.insert("Bruenor Battlehammer".to_string(), Contact { name: "Bruenor Battlehammer".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Mithral Hall".to_string()), notes: Some("Dwarf king, ally.".to_string()) });
contacts.insert("Catti-brie".to_string(), Contact { name: "Catti-brie".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Mithral Hall".to_string()), notes: Some("Skilled archer.".to_string()) });
contacts.insert("Regis".to_string(), Contact { name: "Regis".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Icewind Dale".to_string()), notes: Some("Halfling, sneaky.".to_string()) });
contacts.insert("Artemis Entreri".to_string(), Contact { name: "Artemis Entreri".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Calimport".to_string()), notes: Some("Dangerous assassin.".to_string()) });
contacts.insert("Storm Silverhand".to_string(), Contact { name: "Storm Silverhand".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Shadowdale".to_string()), notes: Some("Bard and Chosen.".to_string()) });
contacts.insert("Halia Thornton".to_string(), Contact { name: "Halia Thornton".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Phandalin".to_string()), notes: Some("Zhentarim agent, subtle.".to_string()) });
contacts.insert("Darlia Greystone".to_string(), Contact { name: "Darlia Greystone".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Triboar".to_string()), notes: Some("Blacksmith, reliable.".to_string()) });
contacts.insert("Eldric Varn".to_string(), Contact { name: "Eldric Varn".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Luskan".to_string()), notes: Some("Ship captain, shady.".to_string()) });
contacts.insert("Tymoras Priest".to_string(), Contact { name: "Tymoras Priest".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Suzail".to_string()), notes: Some("Blesses adventurers.".to_string()) });
contacts.insert("Redbrand Leader".to_string(), Contact { name: "Redbrand Leader".to_string(), image: None, alias: Some("Glasstaff".to_string()), status: Some(ContactStatus::Alive), location: Some("Phandalin".to_string()), notes: Some("Runs thug gang.".to_string()) });
contacts.insert("Brynna of Lathander".to_string(), Contact { name: "Brynna of Lathander".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Suzail".to_string()), notes: Some("Temple cleric.".to_string()) });
contacts.insert("Korgul the Tumbler".to_string(), Contact { name: "Korgul the Tumbler".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Baldur's Gate".to_string()), notes: Some("Thieves guild contact.".to_string()) });
contacts.insert("Tharivol the Mage".to_string(), Contact { name: "Tharivol the Mage".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Silverymoon".to_string()), notes: Some("Studies arcane wards.".to_string()) });
contacts.insert("Gorlag the Orc".to_string(), Contact { name: "Gorlag the Orc".to_string(), image: None, alias: None, status: Some(ContactStatus::Dead), location: Some("Neverwinter Wood".to_string()), notes: Some("Slain orc chieftain.".to_string()) });
contacts.insert("Lhara the Merchant".to_string(), Contact { name: "Lhara the Merchant".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Cormyr".to_string()), notes: Some("Sells rare goods.".to_string()) });
contacts.insert("Ebon the Rogue".to_string(), Contact { name: "Ebon the Rogue".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Zhentil Keep".to_string()), notes: Some("Sneaky informant.".to_string()) });
contacts.insert("Father Llymic".to_string(), Contact { name: "Father Llymic".to_string(), image: None, alias: None, status: Some(ContactStatus::Dead), location: Some("Icewind Dale".to_string()), notes: Some("Evil priest, defeated.".to_string()) });
contacts.insert("Sylvara the Druid".to_string(), Contact { name: "Sylvara the Druid".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("High Forest".to_string()), notes: Some("Protects the woods.".to_string()) });
contacts.insert("Raul the Barkeep".to_string(), Contact { name: "Raul the Barkeep".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Daggerford".to_string()), notes: Some("Hears local gossip.".to_string()) });
// 5 Contacts (10%)
contacts.insert("Valthor the Fence".to_string(), Contact { name: "Valthor the Fence".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Baldur's Gate".to_string()), notes: Some("Buys stolen goods.".to_string()) });
contacts.insert("Zyra the Alchemist".to_string(), Contact { name: "Zyra the Alchemist".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Waterdeep".to_string()), notes: Some("Sells potions.".to_string()) });
contacts.insert("Kren the Smuggler".to_string(), Contact { name: "Kren the Smuggler".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Luskan".to_string()), notes: Some("Moves contraband.".to_string()) });
contacts.insert("Mira the Scribe".to_string(), Contact { name: "Mira the Scribe".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Candlekeep".to_string()), notes: Some("Deciphers texts.".to_string()) });
contacts.insert("Thok the Armorer".to_string(), Contact { name: "Thok the Armorer".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: Some("Neverwinter".to_string()), notes: Some("Crafts fine armor.".to_string()) });
// 3 Present-Day Legends (6%)
contacts.insert("Tiamat".to_string(), Contact { name: "Tiamat".to_string(), image: None, alias: Some("The Dragon Queen".to_string()), status: Some(ContactStatus::Alive), location: None, notes: Some("Evil dragon goddess.".to_string()) });
contacts.insert("Vecna".to_string(), Contact { name: "Vecna".to_string(), image: None, alias: None, status: Some(ContactStatus::Alive), location: None, notes: Some("Lich god of secrets.".to_string()) });
contacts.insert("Claugiyliamatar".to_string(), Contact { name: "Claugiyliamatar".to_string(), image: None, alias: Some("Old Gnawbone".to_string()), status: Some(ContactStatus::Alive), location: Some("Kryptgarden Forest".to_string()), notes: Some("Ancient green dragon.".to_string()) });
// 2 Past Legends (4%, DeadBygone)
contacts.insert("Manshoon".to_string(), Contact { name: "Manshoon".to_string(), image: None, alias: None, status: Some(ContactStatus::DeadBygone), location: Some("Zhentil Keep".to_string()), notes: Some("Zhentarim founder, long dead.".to_string()) });
contacts.insert("Netherese Archwizard".to_string(), Contact { name: "Netherese Archwizard".to_string(), image: None, alias: None, status: Some(ContactStatus::DeadBygone), location: None, notes: Some("Ancient mage of lost empire.".to_string()) });
ContactBook(contacts)
}

184
src/entities/mod.rs Normal file
View File

@ -0,0 +1,184 @@
use chrono::NaiveDate;
use reactive_stores::Store;
use crate::prelude::*;
mod mock;
#[derive(Clone, Debug, Store)]
pub struct Item {
pub name: String,
pub weight: Option<f64>,
pub rarity: Option<String>,
pub rest: Map<String, Value>,
}
impl Item {
pub fn from_json (t: Value) -> Option<Self> {
let Value::String(name) = t["name"].clone() else { return None };
let weight = match t["weight"].clone() {
Value::Number(t) => t.as_f64(),
Value::String(t) => t.parse().ok(),
_ => None
};
let rarity = if let Value::String(ref rarity) = t["rarity"] {
Some(rarity.clone())
} else {
None
};
let Value::Object(rest) = t else { return None };
Some(Self { name, weight, rarity, rest })
}
}
#[derive(Clone, Debug, Store)]
pub struct ItemReferenceMap (pub HashMap<String, Item>);
#[derive(Clone, Debug, Store)]
pub struct Dashboard {
pub campaign: String,
pub date: NaiveDate,
pub player: PlayerData,
pub common: CommonData,
pub quest_book: QuestBook,
pub notes: NoteBook,
pub people: ContactBook,
}
#[derive(Clone, Debug, Store)]
pub struct PlayerData {
pub name: String,
pub image: String,
pub level: u8,
pub xp: u32,
pub temp_hp: u16,
pub hp: u16,
pub max_hp: u16,
pub balance: Balance,
pub spell_slots: SpellSlots,
pub inventory: Inventory,
// pub consumables: Consumables,
pub attunements: Attunements,
pub prepared: PreparedSpells,
pub hit_dice: HitDice,
pub death_save_throws: DeathSaveThrows,
}
#[derive(Clone, Debug, Store)]
pub struct CommonData {
pub balance: Balance,
pub inventory: Inventory,
}
#[derive(Clone, Debug, Store)]
pub struct Balance {
pub platinum: u32,
pub gold: u32,
pub electrum: u32,
pub silver: u32,
pub copper: u32,
}
#[derive(Clone, Debug, Store)]
pub struct Inventory (pub Vec<InventoryEntry>);
#[derive(Clone, Debug, Store)]
pub struct InventoryEntry {
pub quantity: u32,
pub item: InventoryItem,
}
#[derive(Clone, Debug, Store)]
pub enum InventoryItem {
Custom (CustomInventoryItem),
Book (Item),
}
#[derive(Clone, Debug, Store)]
pub struct CustomInventoryItem {
pub name: String,
pub weight: Option<f32>,
pub description: Option<String>,
pub tags: HashMap<String, String>,
}
#[derive(Clone, Debug, Store)]
pub struct QuestBook (pub Vec<Quest>);
#[derive(Clone, Debug, Store)]
pub struct Quest {
pub title: String,
pub location_taken: Option<String>,
pub location_task: Option<String>,
pub date_taken: Option<NaiveDate>,
pub date_by: Option<NaiveDate>,
pub description: Option<String>,
pub from: Option<String>,
pub reward: Option<Balance>,
}
#[derive(Clone, Debug, Store)]
pub struct NoteBook (pub Vec<Note>);
#[derive(Clone, Debug, Store)]
pub struct Note (pub String);
#[derive(Clone, Debug, Store)]
pub struct SpellSlots (pub [SpellSlotLevel; 9]);
#[derive(Clone, Debug, Store)]
pub struct SpellSlotLevel {
pub used: u8,
pub total: u8,
}
#[derive(Clone, Debug, Store)]
pub struct Attunements (pub [Option<String>; 3]);
#[derive(Clone, Debug, Store)]
pub struct PreparedSpells (pub Vec<Spell>);
// @TODO
#[derive(Clone, Debug, Store)]
pub struct Spell {
pub name: String
}
#[derive(Clone, Debug, Store)]
pub struct HitDice {
pub used: u8,
pub total: u8,
}
#[derive(Clone, Debug, Store)]
pub struct DeathSaveThrows {
pub failed: u8,
pub succeeded: u8,
}
#[derive(Clone, Debug, Store)]
pub struct ContactBook (pub HashMap<String, Contact>);
#[derive(Clone, Debug, Store)]
pub struct Contact {
pub name: String,
pub image: Option<String>,
pub alias: Option<String>,
pub status: Option<ContactStatus>,
pub location: Option<String>,
pub notes: Option<String>,
}
#[derive(Clone, Debug, Store)]
pub enum ContactStatus {
Unknown,
DeadBygone,
Dead,
Alive,
}

View File

@ -1,5 +1,23 @@
pub mod app;
use leptos::prelude::*;
#[macro_use]
extern crate log;
#[allow(unused_imports)]
#[macro_use]
extern crate anyhow;
pub (crate) mod prelude;
pub mod entities;
pub mod state;
pub mod utils;
pub mod statics;
mod components;
use prelude::*;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {

View File

@ -1,3 +1,4 @@
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {

19
src/prelude.rs Normal file
View File

@ -0,0 +1,19 @@
#![allow(unused_imports)]
pub use std::collections::{ HashMap, HashSet };
pub use serde_json::{ Value, Map };
pub use log::{ warn as w, log as l, info as i, error as e, trace as t, debug as d };
pub use anyhow::{ anyhow, bail };
pub use reactive_stores::Store;
pub use crate::{
utils,
entities::{ ItemReferenceMap, Item, Dashboard },
statics::*,
};
pub type Error = anyhow::Error;
pub type Result <T> = anyhow::Result<T>;
pub type Context = Store<Dashboard>;

1
src/state/mod.rs Normal file
View File

@ -0,0 +1 @@
use crate::prelude::*;

23
src/statics/mod.rs Normal file
View File

@ -0,0 +1,23 @@
use crate::prelude::*;
use lazy_static::lazy_static;
lazy_static! {
pub static ref ITEMS_REFS: ItemReferenceMap = {
let mut map = HashMap::with_capacity(5_000);
utils::read_items(include_str!("../../data/items.json"), &mut map);
utils::read_items(include_str!("../../data/base.json"), &mut map);
utils::read_items(include_str!("../../data/foundry.json"), &mut map);
utils::read_items(include_str!("../../data/magic.json"), &mut map);
utils::read_items(include_str!("../../data/foundry.json"), &mut map);
info!("Loaded {} items", map.len());
ItemReferenceMap(HashMap::from_iter(map.into_iter()))
};
pub static ref ITEMS_NAMES: Vec<&'static String> = {
ITEMS_REFS.0.keys().collect()
};
}

21
src/utils/mod.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::prelude::*;
pub fn read_items (contents: &str, map: &mut HashMap<String, Item>) -> usize {
let Ok(json): std::result::Result<Value, _> = 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 };
let start = map.len();
for t in list.into_iter().map(Item::from_json).flatten() {
if map.contains_key(&t.name) {
let entry = map.get_mut(&t.name).unwrap();
entry.weight = entry.weight.or(t.weight);
entry.rest.extend(t.rest);
} else {
map.insert(t.name.clone(), t);
}
}
map.len() - start
}

View File

@ -1,4 +1,21 @@
@use 'reset';
body {
font-family: sans-serif;
text-align: center;
}
font-size: 1.22em;
}
header {
display: grid;
}
input {
border: 1px solid black;
height: 36px;
font-size: 28px;
padding: 3px;
}
.item-search-card {
display: flex;
flex-direction: row;
}

940
style/reset.scss Normal file
View File

@ -0,0 +1,940 @@
@namespace svg "http://www.w3.org/2000/svg";
/**
* Total Reset
* ==================================================
* Universal reset of styles for all elements and pseudo-elements for customizing Web Components and browser extensions.
*/
/* Selector targets all elements except tables and svg elements */
*:where(:not(table, thead, tbody, tr, th, td, svg|*)) {
/* Resets all styles for the selected elements */
all: unset;
/* Set box-sizing to border-box so padding and borders do not affect the total width and height of elements */
box-sizing: border-box;
/* Resets styles for ::before and ::after pseudo-elements */
}
*:where(:not(table, thead, tbody, tr, th, td, svg|*))::before, *:where(:not(table, thead, tbody, tr, th, td, svg|*))::after {
/* Unsets all styles for pseudo-elements */
all: unset;
/* Set box-sizing to border-box for pseudo-elements */
box-sizing: border-box;
}
a,
abbr,
acronym,
address,
article,
aside,
audio,
b,
big,
blockquote,
button,
canvas,
caption,
center,
cite,
code,
dd,
del,
details,
dfn,
div,
dl,
dt,
em,
embed,
fieldset,
figcaption,
figure,
footer,
form,
h1,
h2,
h3,
h4,
h5,
h6,
header,
hgroup,
i,
iframe,
img,
ins,
kbd,
label,
legend,
li,
main,
mark,
menu,
nav,
ol,
output,
p,
pre,
q,
ruby,
s,
samp,
section,
small,
span,
strike,
strong,
sub,
summary,
sup,
table,
tbody,
td,
tfoot,
th,
thead,
time,
tr,
tt,
u,
ul,
var,
video {
font-size: 100%;
}
article,
aside,
blockquote,
details,
div,
fieldset,
figcaption,
figure,
footer,
form,
h1,
h2,
h3,
h4,
h5,
h6,
header,
hgroup,
main,
menu,
nav,
p,
pre,
section {
display: block;
}
audio,
canvas,
video,
img,
picture,
svg {
display: inline-block;
max-width: 100%;
vertical-align: middle;
}
canvas,
iframe {
display: block;
}
[hidden] {
display: none;
}
head,
link,
meta,
script,
title,
template,
style {
display: none;
}
a[href],
label[for],
select,
button {
cursor: pointer;
}
/**
* Table
*/
table {
border-collapse: collapse;
border-spacing: 0;
text-indent: 0;
}
table,
thead,
tbody,
tr,
th,
td {
font-size: 100%;
font: inherit;
margin: 0;
padding: 0;
border: 0;
vertical-align: baseline;
}
/**
* Forms
*/
input {
appearance: none;
display: inline-block;
}
input[type=color] {
width: 15px;
height: 15px;
}
input[type=color]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type=color]::-webkit-color-swatch {
border: none;
}
input:required,
input {
box-shadow: none;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px white inset;
}
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-results-button,
input[type=search]::-webkit-search-results-decoration {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type=search] {
-webkit-appearance: none;
-moz-appearance: none;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
input:focus {
outline: none;
}
/**
* Only apply smooth scrolling when the user hasn't set their motion preference to "reduce"
*/
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
/*# sourceMappingURL=total-reset.css.map */
/**
* Modern CSS Reset Tweaks
* ==================================================
* A collection of modern CSS reset and normalization styles
* to ensure consistent behavior across browsers, OS and devices.
*/
/* Ensure consistent font resizing on mobile devices */
html {
-webkit-text-size-adjust: 100%;
}
html:focus-within {
scroll-behavior: smooth;
}
/* Basic body setup for layout and text rendering optimization */
body {
text-size-adjust: 100%;
position: relative;
width: 100%;
min-height: 100vh;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeSpeed;
}
/* Apply box-sizing globally for consistent element sizing */
*,
::after,
::before {
box-sizing: border-box;
}
/* Style unclassed links for better accessibility */
a:not([class]) {
text-decoration-skip-ink: auto;
}
/**
* CSS Reset Tweaks
* Based on Eric Meyer's CSS Reset v2.0-modified (public domain)
* URL: http://meyerweb.com/eric/tools/css/reset/
*/
a,
abbr,
acronym,
address,
applet,
article,
aside,
audio,
b,
big,
blockquote,
body,
br,
button,
canvas,
caption,
center,
cite,
code,
col,
colgroup,
data,
datalist,
dd,
del,
details,
dfn,
div,
dl,
dt,
em,
embed,
fieldset,
figcaption,
figure,
footer,
form,
h1,
h2,
h3,
h4,
h5,
h6,
head,
header,
hgroup,
hr,
html,
i,
iframe,
img,
input,
ins,
kbd,
label,
legend,
li,
link,
main,
map,
mark,
menu,
meta,
meter,
nav,
noscript,
object,
ol,
optgroup,
option,
output,
p,
param,
picture,
pre,
progress,
q,
rb,
rp,
rt,
rtc,
ruby,
s,
samp,
script,
section,
select,
small,
source,
span,
strong,
style,
svg,
sub,
summary,
sup,
table,
tbody,
td,
template,
textarea,
tfoot,
th,
thead,
time,
title,
tr,
track,
tt,
u,
ul,
var,
video,
wbr {
font-size: 100%;
font: inherit;
margin: 0;
padding: 0;
border: 0;
vertical-align: baseline;
}
/* Add focus styles to improve accessibility */
:focus {
outline: 0;
}
/* Normalize HTML5 elements for older browsers */
article,
aside,
details,
embed,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
object,
section {
display: block;
}
canvas,
iframe {
max-width: 100%;
height: auto;
display: block;
}
/* Remove default list styling */
ol,
ul {
list-style: none;
}
/* Normalize quote styling */
blockquote,
q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
/* Reset and normalize form inputs */
input:required,
input {
box-shadow: none;
}
/* Autofill styling for better compatibility */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px white inset;
}
/* Improve appearance of search inputs */
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-results-button,
input[type=search]::-webkit-search-results-decoration {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type=search] {
-webkit-appearance: none;
-moz-appearance: none;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
input:focus {
outline: none;
}
video {
background: #000;
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
*/
[hidden] {
display: none;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: none;
}
/**
* Make media easier to work with
*/
audio,
img,
picture,
svg,
video {
max-width: 100%;
display: inline-block;
vertical-align: middle;
height: auto;
}
/**
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
* Correct `select` style inheritance in Firefox 4+ and Opera.
*/
button,
select {
text-transform: none;
}
button,
html input[type=button],
input[type=reset],
input[type=submit] {
-webkit-appearance: button;
cursor: pointer;
border: 0;
background: transparent;
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/* Additional attribute handling for accessibility */
[disabled],
[disabled=true],
[aria-disabled=true] {
pointer-events: none;
}
/**
* Address box sizing set to content-box in IE 8/9.
*/
input[type=checkbox],
input[type=radio] {
padding: 0;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type=search] {
-webkit-appearance: textfield;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Remove inner padding and border in Firefox 3+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
button {
border: 0;
background: transparent;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
text-indent: 0;
}
/**
* Based on normalize.css v8.0.1
* github.com/necolas/normalize.css
*/
hr {
box-sizing: content-box;
overflow: visible;
background: #000;
border: 0;
height: 1px;
line-height: 0;
margin: 0;
padding: 0;
page-break-after: always;
width: 100%;
}
/**
* Correct the inheritance and scaling of font size in all browsers.
*/
pre {
font-family: monospace, monospace;
font-size: 100%;
}
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
text-decoration: none;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 75%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -5px;
}
sup {
top: -5px;
}
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1;
margin: 0;
padding: 0;
}
/**
* Show the overflow in IE and Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type=button]::-moz-focus-inner,
[type=reset]::-moz-focus-inner,
[type=submit]::-moz-focus-inner {
border-style: none;
padding: 0;
outline: 0;
}
legend {
color: inherit;
white-space: normal;
display: block;
border: 0;
max-width: 100%;
width: 100%;
}
fieldset {
min-width: 0;
}
body:not(:-moz-handler-blocked) fieldset {
display: block;
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type=number]::-webkit-inner-spin-button,
[type=number]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type=search] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type=search]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
template {
display: none;
}
/**
* Variables
*/
/*
* Fonts
* ========================================================================== */
/*
* Palette
* ========================================================================== */
/*
* Basic Colors
* ========================================================================== */
/*
* Mixing Colors
* ========================================================================== */
/**
* Typography
*/
html,
body {
font-family: Arial, Helvetica, sans-serif;
font-weight: 400;
}
html {
font-size: 100%;
}
body {
color: #000;
font-size: 16px;
line-height: 24px;
}
h1,
h2,
h3 {
font-family: Arial, Helvetica, sans-serif;
font-weight: 600;
}
html,
button,
input,
select,
textarea {
color: #000;
}
a {
color: #000;
text-decoration: none;
}
a:visited {
outline: 0;
}
a:focus, a:active, a:hover {
text-decoration: underline;
outline: 0;
}
i,
em {
font-style: italic;
}
b,
strong {
font-weight: bold;
}
.br {
display: block;
}
/*# sourceMappingURL=main.css.map */