feat(ui): add simulation about dialog

Co-Authored-By: GitHub Copilot <noreply@github.com>
This commit is contained in:
Steve Biedermann 2026-10-13 17:57:17 +02:00
parent 7aebac994b
commit 71b3e44e8a
4 changed files with 113 additions and 2 deletions

View File

@ -6,6 +6,7 @@ Notable changes to this project are documented here.
### Added ### Added
- Add an About dialog explaining the simulation, shards, actors, and persistence.
- Docker Compose runs the gateway and each shard in its own container, with one persistent SQLite volume per shard. - Docker Compose runs the gateway and each shard in its own container, with one persistent SQLite volume per shard.
- Public clients are view-only: the UI hides editing controls and the server rejects remote writes; localhost clients retain editing access. - Public clients are view-only: the UI hides editing controls and the server rejects remote writes; localhost clients retain editing access.
- Expose the Picard actor console over the `/api/actors/ws` WebSocket endpoint. - Expose the Picard actor console over the `/api/actors/ws` WebSocket endpoint.

View File

@ -11,7 +11,7 @@ leptos = { version = "0.8", features = ["csr"] }
wasm-bindgen = "=0.2.129" wasm-bindgen = "=0.2.129"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
js-sys = "0.3" js-sys = "0.3"
web-sys = { version = "0.3", features = ["CanvasRenderingContext2d", "HtmlCanvasElement", "HtmlElement", "MouseEvent", "WheelEvent", "AddEventListenerOptions", "EventTarget", "WebSocket", "MessageEvent", "Location", "Element", "DomRect", "HtmlDivElement", "Window", "Storage"] } web-sys = { version = "0.3", features = ["CanvasRenderingContext2d", "HtmlCanvasElement", "HtmlElement", "HtmlDialogElement", "MouseEvent", "WheelEvent", "AddEventListenerOptions", "EventTarget", "WebSocket", "MessageEvent", "Location", "Element", "DomRect", "HtmlDivElement", "Window", "Storage"] }
gloo-net = { version = "0.6", default-features = false, features = ["http", "json"] } gloo-net = { version = "0.6", default-features = false, features = ["http", "json"] }
gloo-timers = { version = "0.3", features = ["futures"] } gloo-timers = { version = "0.3", features = ["futures"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }

View File

@ -40,6 +40,7 @@ fn App() -> impl IntoView {
let param = RwSignal::new(String::new()); let param = RwSignal::new(String::new());
let amount = RwSignal::new("5".to_string()); let amount = RwSignal::new("5".to_string());
let can_edit = RwSignal::new(false); let can_edit = RwSignal::new(false);
let about_dialog = NodeRef::<leptos::html::Dialog>::new();
// (selection counter, people at the time of selection); the counter forces editor rows to rebuild. // (selection counter, people at the time of selection); the counter forces editor rows to rebuild.
let editing = RwSignal::new((0u64, Vec::<Person>::new())); let editing = RwSignal::new((0u64, Vec::<Person>::new()));
@ -293,7 +294,49 @@ fn App() -> impl IntoView {
></canvas> ></canvas>
</div> </div>
<div class="side"> <div class="side">
<h3>"World"</h3> <div class="panel-heading">
<h3>"World"</h3>
<button on:click=move |_| {
if let Some(dialog) = about_dialog.get() {
let _ = dialog.show_modal();
}
} aria-label="About World Simulation">"About"</button>
</div>
<dialog class="about-dialog" node_ref=about_dialog>
<div class="about-header">
<div>
<p class="about-kicker">"WORLD SIMULATION"</p>
<h2>"A living world, running in shards"</h2>
</div>
<button
class="about-close"
on:click=move |_| {
if let Some(dialog) = about_dialog.get() {
dialog.close();
}
}
aria-label="Close about dialog"
>"Close"</button>
</div>
<p>
"This is an ongoing simulation of a small world. People and wildlife move, gather resources, work, trade, form families, and respond to the rules in the world's Rune behavior script."
</p>
<h3>"One world, several shards"</h3>
<p>
"The map is divided into vertical regions called shards. Each shard runs its own part of the simulation, and entities that cross a boundary are handed to the shard that owns their new position. The gateway coordinates their steps and combines their views into this map."
</p>
<h3>"Actors and persistence"</h3>
<p>
"Each person, animal, partnership, market, workplace, and cart is managed by an actor. Shards save their own world snapshots, so the simulation can resume after a restart."
</p>
<div class="about-actions">
<button on:click=move |_| {
if let Some(dialog) = about_dialog.get() {
dialog.close();
}
} autofocus=true>"Done"</button>
</div>
</dialog>
{move || state.with(|s| s.as_ref().map(|s| { {move || state.with(|s| s.as_ref().map(|s| {
let (h, m) = (s.hour.floor(), (s.hour.fract() * 60.0).floor()); let (h, m) = (s.hour.floor(), (s.hour.fract() * 60.0).floor());
view! { view! {

View File

@ -48,6 +48,73 @@ canvas {
box-sizing: border-box; box-sizing: border-box;
} }
.panel-heading {
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-heading h3 {
margin: 1em 0;
}
.about-dialog {
width: min(560px, calc(100vw - 40px));
max-height: min(640px, calc(100vh - 40px));
padding: 24px;
overflow: auto;
border: 1px solid #56615c;
border-top: 3px solid #9bbf8a;
border-radius: 4px;
background: #171b19;
color: #ddd;
font: inherit;
line-height: 1.6;
}
.about-dialog::backdrop {
background: rgb(0 0 0 / 72%);
}
.about-dialog h2 {
margin: 4px 0 12px;
font-size: 20px;
line-height: 1.3;
}
.about-dialog h3 {
margin: 22px 0 4px;
color: #b7d1a8;
font-size: 14px;
}
.about-dialog p {
margin: 8px 0;
color: #c2c9c4;
}
.about-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.about-kicker {
color: #a7c895 !important;
font-size: 11px;
}
.about-close {
flex: none;
}
.about-actions {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.side input[type=number] { .side input[type=number] {
width: 56px; width: 56px;
} }