From 71b3e44e8a9277cba987f2a9fdc4440871c08d80 Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Tue, 13 Oct 2026 17:57:17 +0200 Subject: [PATCH] feat(ui): add simulation about dialog Co-Authored-By: GitHub Copilot --- CHANGELOG.md | 1 + ui/Cargo.toml | 2 +- ui/src/lib.rs | 45 +++++++++++++++++++++++++++++++++- ui/style.css | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80bf830..792a208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Notable changes to this project are documented here. ### 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. - 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. diff --git a/ui/Cargo.toml b/ui/Cargo.toml index fcfb1be..276c61f 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -11,7 +11,7 @@ leptos = { version = "0.8", features = ["csr"] } wasm-bindgen = "=0.2.129" wasm-bindgen-futures = "0.4" 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-timers = { version = "0.3", features = ["futures"] } serde = { version = "1", features = ["derive"] } diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 5bdc8df..fd47b0f 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -40,6 +40,7 @@ fn App() -> impl IntoView { let param = RwSignal::new(String::new()); let amount = RwSignal::new("5".to_string()); let can_edit = RwSignal::new(false); + let about_dialog = NodeRef::::new(); // (selection counter, people at the time of selection); the counter forces editor rows to rebuild. let editing = RwSignal::new((0u64, Vec::::new())); @@ -293,7 +294,49 @@ fn App() -> impl IntoView { >
-

"World"

+
+

"World"

+ +
+ +
+
+

"WORLD SIMULATION"

+

"A living world, running in shards"

+
+ +
+

+ "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." +

+

"One world, several shards"

+

+ "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." +

+

"Actors and persistence"

+

+ "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." +

+
+ +
+
{move || state.with(|s| s.as_ref().map(|s| { let (h, m) = (s.hour.floor(), (s.hour.fract() * 60.0).floor()); view! { diff --git a/ui/style.css b/ui/style.css index e95cbee..bd76e1c 100644 --- a/ui/style.css +++ b/ui/style.css @@ -48,6 +48,73 @@ canvas { 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] { width: 56px; }