- PlayerCard removed — it duplicated the sidebar editors; the character page now shows only prepared spells and attunements - headings are plain RU: Заготовленные заклинания / Атюнменты (no EN/RU double labels; h6 no longer lowercased) - Attunements is Vec<Option<String>> instead of a fixed 3-slot array; '+ слот' appends a slot, so custom campaigns can track more items (old saved JSON parses: arrays map to Vec) - PlayerData.ac (u8, serde default 0): manual armor-class input in the sidebar HP block (hp/оз, ac/КД), editable like the HP numbers
73 lines
2.4 KiB
Rust
73 lines
2.4 KiB
Rust
use reactive_stores::Field;
|
||
|
||
use crate::prelude::*;
|
||
use leptos::prelude::*;
|
||
|
||
/// The three attunement slots: each is either empty (free-text input and
|
||
/// "+" button) or holds an item name with a "×" clear button.
|
||
#[component]
|
||
pub fn Attunements () -> impl IntoView {
|
||
let state = expect_context::<Context>();
|
||
let attunements: Field<Attunements> = state.player().attunements().into();
|
||
|
||
view! {
|
||
<div class="attunements">
|
||
<h6>"Атюнменты:"</h6>
|
||
<For
|
||
// Same state-in-key convention as prepared spells, so a
|
||
// slot whose value changed re-renders.
|
||
each=move || attunements.get().0.into_iter().enumerate()
|
||
key=|(i, s)| format!("{}-{}", i, s.as_deref().unwrap_or(""))
|
||
let((idx, slot))
|
||
>
|
||
{slot_row(attunements, idx, slot)}
|
||
</For>
|
||
<button
|
||
class="add-slot"
|
||
title="Добавить слот привязки"
|
||
on:click=move |_| attunements.update(|a| a.0.push(None))
|
||
>
|
||
"+ слот"
|
||
</button>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
/// One attunement slot: the free-text entry (empty) or the item itself.
|
||
/// A plain function: component tags are not allowed directly inside
|
||
/// `For`'s `let(...)` children.
|
||
fn slot_row (
|
||
attunements: Field<Attunements>,
|
||
idx: usize,
|
||
slot: Option<String>,
|
||
) -> impl IntoView {
|
||
let text_sig = RwSignal::new(String::new());
|
||
|
||
// Binds the typed item name; empty text is ignored.
|
||
let bind = move |_| {
|
||
let text = text_sig.get_untracked();
|
||
if text.trim().is_empty() {
|
||
return;
|
||
}
|
||
attunements.update(|a| a.0[idx] = Some(text.trim().to_string()));
|
||
};
|
||
|
||
let clear = move |_| attunements.update(|a| a.0[idx] = None);
|
||
|
||
view! {
|
||
<div class="attunement-slot">
|
||
{if slot.is_some() {
|
||
view! {
|
||
<div class="attuned-item">{slot.clone().unwrap_or_default()}</div>
|
||
<button title="Снять привязку" on:click=clear>"×"</button>
|
||
}.into_any()
|
||
} else {
|
||
view! {
|
||
<input type="text" placeholder="Предмет…" bind:value=text_sig />
|
||
<button title="Привязать предмет" on:click=bind>"+"</button>
|
||
}.into_any()
|
||
}}
|
||
</div>
|
||
}
|
||
}
|