character page: player card, prepared spells, attunements
- PlayerCard: read-only portrait, name, class, level, XP (the sidebar
holds the editors for this data)
- PreparedSpells: growable slot list; a vacant slot has a free-text
input and '+' to fill it, an occupied slot shows name/level/
description and '×' to empty it; '+ слот' appends a vacant slot
- Attunements: three slots, free-text entry and per-slot clear
- rows are plain functions called from For's let(...) children —
component tags are not allowed directly inside them (view macro)
- Field<...> conversions for the Subfield prop types, state-in-key
keys so changed rows re-render (same convention as sidebar pips)
- SCSS: _character.scss with card, slot rows, buttons (mixins)
2026-08-16 09:43:52 +00:00
|
|
|
|
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">
|
2026-08-16 10:07:07 +00:00
|
|
|
|
<h6>"Атюнменты:"</h6>
|
character page: player card, prepared spells, attunements
- PlayerCard: read-only portrait, name, class, level, XP (the sidebar
holds the editors for this data)
- PreparedSpells: growable slot list; a vacant slot has a free-text
input and '+' to fill it, an occupied slot shows name/level/
description and '×' to empty it; '+ слот' appends a vacant slot
- Attunements: three slots, free-text entry and per-slot clear
- rows are plain functions called from For's let(...) children —
component tags are not allowed directly inside them (view macro)
- Field<...> conversions for the Subfield prop types, state-in-key
keys so changed rows re-render (same convention as sidebar pips)
- SCSS: _character.scss with card, slot rows, buttons (mixins)
2026-08-16 09:43:52 +00:00
|
|
|
|
<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>
|
2026-08-16 10:07:07 +00:00
|
|
|
|
<button
|
|
|
|
|
|
class="add-slot"
|
|
|
|
|
|
title="Добавить слот привязки"
|
|
|
|
|
|
on:click=move |_| attunements.update(|a| a.0.push(None))
|
|
|
|
|
|
>
|
|
|
|
|
|
"+ слот"
|
|
|
|
|
|
</button>
|
character page: player card, prepared spells, attunements
- PlayerCard: read-only portrait, name, class, level, XP (the sidebar
holds the editors for this data)
- PreparedSpells: growable slot list; a vacant slot has a free-text
input and '+' to fill it, an occupied slot shows name/level/
description and '×' to empty it; '+ слот' appends a vacant slot
- Attunements: three slots, free-text entry and per-slot clear
- rows are plain functions called from For's let(...) children —
component tags are not allowed directly inside them (view macro)
- Field<...> conversions for the Subfield prop types, state-in-key
keys so changed rows re-render (same convention as sidebar pips)
- SCSS: _character.scss with card, slot rows, buttons (mixins)
2026-08-16 09:43:52 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|