feat(examples): add flagship techdemo
req: examples/001 req: dx/008 req: form/002 req: page_swap/002 req: push/003
This commit is contained in:
Generated
+13
@@ -608,6 +608,19 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slhx-techdemo"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"futures-util",
|
||||||
|
"slhx",
|
||||||
|
"slhx-axum",
|
||||||
|
"slhx-build",
|
||||||
|
"slhx-test",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slhx-test"
|
name = "slhx-test"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["slhx", "slhx-core", "slhx-derive", "slhx-js", "slhx-axum", "slhx-build", "slhx-test", "examples/v0", "examples/kanban"]
|
members = ["slhx", "slhx-core", "slhx-derive", "slhx-js", "slhx-axum", "slhx-build", "slhx-test", "examples/v0", "examples/kanban", "examples/techdemo"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "slhx-techdemo"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.7"
|
||||||
|
futures-util = "0.3"
|
||||||
|
slhx = { path = "../../slhx" }
|
||||||
|
slhx-axum = { path = "../../slhx-axum" }
|
||||||
|
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
slhx-test = { path = "../../slhx-test" }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
slhx-build = { path = "../../slhx-build" }
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# slhx full techdemo
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
cargo run -p slhx-techdemo
|
||||||
|
|
||||||
|
Open <http://127.0.0.1:3002>.
|
||||||
|
|
||||||
|
This demo is tailored to showcase slhx strengths:
|
||||||
|
|
||||||
|
- modern SSR-first UI
|
||||||
|
- generated resource modules from `.heml`
|
||||||
|
- native form posts carrying `__h` numeric handle ids
|
||||||
|
- multi-target `application/slhx` EffectBatch responses
|
||||||
|
- generated slot updates instead of selectors
|
||||||
|
- root-scoped runtime lowering (`data-hid`, `data-sid`)
|
||||||
|
- page-enhancer navigation with native link fallback
|
||||||
|
- SSE server push into a generated slot
|
||||||
|
- no user-authored browser JavaScript
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
slhx_build::app().run().unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#[slhx::surface]
|
||||||
|
pub mod ui {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ui;
|
||||||
|
use slhx::{IntoEffect, SafeHtml};
|
||||||
|
use slhx_test::inspect;
|
||||||
|
|
||||||
|
// req: examples/001 req: codegen/002 req: public_api/001
|
||||||
|
#[test]
|
||||||
|
fn techdemo_uses_generated_slots_for_multi_target_updates() {
|
||||||
|
fn update() -> impl IntoEffect {
|
||||||
|
(
|
||||||
|
ui::control_center::slots::hero_metrics.html(SafeHtml::trusted("<b>fast</b>")),
|
||||||
|
ui::control_center::slots::notice.text("typed"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let batch = inspect(update());
|
||||||
|
assert!(batch.has_slot(ui::control_center::slots::hero_metrics));
|
||||||
|
assert!(batch.has_slot(ui::control_center::slots::notice));
|
||||||
|
}
|
||||||
|
|
||||||
|
// req: examples/001 req: form/002 req: codegen/003
|
||||||
|
#[test]
|
||||||
|
fn techdemo_exports_form_and_interaction_handles() {
|
||||||
|
assert_ne!(ui::control_center::handles::launch_work.id().id, ui::control_center::handles::advance_work.id().id);
|
||||||
|
assert_eq!(
|
||||||
|
ui::control_center::forms::launch_work.field("title").resource,
|
||||||
|
ui::control_center::forms::launch_work.id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
use axum::extract::{Query, State};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::Router;
|
||||||
|
use futures_util::{stream, StreamExt};
|
||||||
|
use slhx::{IntoEffect, SafeHtml};
|
||||||
|
use slhx_axum::{runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
|
||||||
|
use slhx_techdemo::ui;
|
||||||
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const LANES: [(&str, &str, &str); 3] = [
|
||||||
|
("compiler", "Compiler", "Surface → generated API"),
|
||||||
|
("runtime", "Runtime", "EffectBatch → DOM"),
|
||||||
|
("product", "Product", "Native UX, zero app JS"),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct WorkItem {
|
||||||
|
id: u64,
|
||||||
|
title: String,
|
||||||
|
lane: usize,
|
||||||
|
impact: u8,
|
||||||
|
stage: Stage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||||
|
enum Stage {
|
||||||
|
Draft,
|
||||||
|
Active,
|
||||||
|
Shipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stage {
|
||||||
|
fn advance(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Draft => Self::Active,
|
||||||
|
Self::Active => Self::Shipped,
|
||||||
|
Self::Shipped => Self::Shipped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Draft => "Draft",
|
||||||
|
Self::Active => "Active",
|
||||||
|
Self::Shipped => "Shipped",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct DemoState {
|
||||||
|
next_id: u64,
|
||||||
|
work: Vec<WorkItem>,
|
||||||
|
activity: VecDeque<String>,
|
||||||
|
spotlight: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DemoState {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut state = Self {
|
||||||
|
next_id: 4,
|
||||||
|
work: vec![
|
||||||
|
WorkItem { id: 1, title: "Compile checked handles".into(), lane: 0, impact: 9, stage: Stage::Shipped },
|
||||||
|
WorkItem { id: 2, title: "Stream typed presence".into(), lane: 1, impact: 7, stage: Stage::Active },
|
||||||
|
WorkItem { id: 3, title: "Replace dashboard widgets".into(), lane: 2, impact: 8, stage: Stage::Draft },
|
||||||
|
],
|
||||||
|
activity: VecDeque::new(),
|
||||||
|
spotlight: "No selectors. Generated resources address every target.".into(),
|
||||||
|
};
|
||||||
|
state.log("Demo booted from server-rendered HTML");
|
||||||
|
state.log("Runtime attached one delegated listener per root");
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DemoState {
|
||||||
|
fn log(&mut self, message: impl Into<String>) {
|
||||||
|
self.activity.push_front(message.into());
|
||||||
|
while self.activity.len() > 6 {
|
||||||
|
self.activity.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Shared {
|
||||||
|
demo: Mutex<DemoState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let state = Arc::new(Shared { demo: Mutex::new(DemoState::default()) });
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(home).post(interact))
|
||||||
|
.route("/architecture", get(architecture))
|
||||||
|
.route("/events", get(events))
|
||||||
|
.route("/slhx.js", get(runtime))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3002));
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
println!("slhx techdemo: http://{addr}");
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// req: examples/001 req: dx/008 req: public_api/001
|
||||||
|
async fn home(State(state): State<Arc<Shared>>, request: PageRequest) -> impl IntoResponse {
|
||||||
|
let demo = state.demo.lock().unwrap().clone();
|
||||||
|
request
|
||||||
|
.page(page_html(&demo), shell)
|
||||||
|
.title("slhx Techdemo")
|
||||||
|
.fingerprint(ui::BUILD_FINGERPRINT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// req: page_swap/001 req: page_swap/002 req: examples/001
|
||||||
|
async fn architecture(request: PageRequest) -> impl IntoResponse {
|
||||||
|
let body = ui::control_center::lower_html(include_str!("../templates/control_center.heml"))
|
||||||
|
.replace("__HERO__", &architecture_hero())
|
||||||
|
.replace("__BOARD__", &architecture_board())
|
||||||
|
.replace("__INSPECTOR__", &architecture_inspector())
|
||||||
|
.replace("__ACTIVITY__", &architecture_activity());
|
||||||
|
request
|
||||||
|
.page(body, shell)
|
||||||
|
.title("slhx Architecture")
|
||||||
|
.fingerprint(ui::BUILD_FINGERPRINT)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn runtime() -> impl IntoResponse {
|
||||||
|
runtime_js()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn interact(
|
||||||
|
State(state): State<Arc<Shared>>,
|
||||||
|
form: InteractionForm,
|
||||||
|
) -> Result<EffectResponse, DispatchRejection> {
|
||||||
|
registry(state).dispatch(form)
|
||||||
|
}
|
||||||
|
|
||||||
|
// req: push/001 req: push/003 req: examples/001
|
||||||
|
async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResponse {
|
||||||
|
if params.contains_key("once") {
|
||||||
|
let effect = ui::control_center::slots::live_feed.html(SafeHtml::trusted(live_feed(1)));
|
||||||
|
return sse(stream::iter([Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT))]).boxed());
|
||||||
|
}
|
||||||
|
|
||||||
|
let batches = stream::unfold(1_u64, |tick| async move {
|
||||||
|
tokio::time::sleep(Duration::from_secs(4)).await;
|
||||||
|
let effect = ui::control_center::slots::live_feed.html(SafeHtml::trusted(live_feed(tick)));
|
||||||
|
Some((Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT)), tick + 1))
|
||||||
|
})
|
||||||
|
.boxed();
|
||||||
|
sse(batches)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
||||||
|
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
||||||
|
.register(ui::control_center::handles::launch_work.id().id, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |form| {
|
||||||
|
// req: form/002 req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
let title = form.value("title").unwrap_or("").trim();
|
||||||
|
let lane = parse_lane(form.value("lane"));
|
||||||
|
let impact = form.value("impact").and_then(|value| value.parse::<u8>().ok()).unwrap_or(5).clamp(1, 9);
|
||||||
|
if !title.is_empty() {
|
||||||
|
let id = demo.next_id;
|
||||||
|
demo.next_id += 1;
|
||||||
|
demo.work.push(WorkItem { id, title: title.into(), lane, impact, stage: Stage::Draft });
|
||||||
|
demo.spotlight = format!("Form data became typed Rust state; card #{id} was rendered by a generated slot.");
|
||||||
|
demo.log(format!("Launched card #{id}: {title}"));
|
||||||
|
}
|
||||||
|
demo_effects(&demo, "Launch accepted · 4 targets updated")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register(ui::control_center::handles::advance_work.id().id, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |form| {
|
||||||
|
// req: list/003 req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
let title = update_work(&mut demo, form.value("work_id"), |item| {
|
||||||
|
item.stage = item.stage.advance();
|
||||||
|
if item.stage == Stage::Shipped {
|
||||||
|
item.lane = LANES.len() - 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(title) = title {
|
||||||
|
demo.spotlight = format!("{title} advanced without a selector: the server returned slot effects.");
|
||||||
|
demo.log(format!("Advanced {title}"));
|
||||||
|
}
|
||||||
|
demo_effects(&demo, "Pipeline advanced")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register(ui::control_center::handles::delete_work.id().id, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |form| {
|
||||||
|
// req: list/003 req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
if let Some(id) = form.value("work_id").and_then(|value| value.parse::<u64>().ok()) {
|
||||||
|
let before = demo.work.len();
|
||||||
|
demo.work.retain(|item| item.id != id);
|
||||||
|
if demo.work.len() < before {
|
||||||
|
demo.spotlight = format!("Card #{id} removed; the board, metrics, activity, and inspector updated together.");
|
||||||
|
demo.log(format!("Deleted card #{id}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
demo_effects(&demo, "Card removed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register(ui::control_center::handles::spotlight_work.id().id, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |form| {
|
||||||
|
// req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
if let Some(id) = form.value("work_id").and_then(|value| value.parse::<u64>().ok()) {
|
||||||
|
if let Some(item) = demo.work.iter().find(|item| item.id == id) {
|
||||||
|
demo.spotlight = format!("{} · lane={} · stage={} · impact={}", item.title, LANES[item.lane].1, item.stage.label(), item.impact);
|
||||||
|
demo.log(format!("Inspected card #{id}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
demo_effects(&demo, "Inspector focused")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register(ui::control_center::handles::simulate_push.id().id, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |_| {
|
||||||
|
// req: push/003 req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
demo.log("Simulated push event produced the same EffectBatch shape");
|
||||||
|
(
|
||||||
|
ui::control_center::slots::live_feed.html(SafeHtml::trusted(live_feed(demo.activity.len() as u64))),
|
||||||
|
ui::control_center::slots::activity.html(SafeHtml::trusted(render_activity(&demo))),
|
||||||
|
ui::control_center::slots::notice.text("Push simulated · no client app code"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register(ui::control_center::handles::reset_demo.id().id, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |_| {
|
||||||
|
// req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
*demo = DemoState::default();
|
||||||
|
demo_effects(&demo, "Demo reset from Rust state")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_work(demo: &mut DemoState, id: Option<&str>, update: impl FnOnce(&mut WorkItem)) -> Option<String> {
|
||||||
|
let id = id.and_then(|value| value.parse::<u64>().ok())?;
|
||||||
|
let item = demo.work.iter_mut().find(|item| item.id == id)?;
|
||||||
|
let title = item.title.clone();
|
||||||
|
update(item);
|
||||||
|
Some(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect {
|
||||||
|
(
|
||||||
|
ui::control_center::slots::hero_metrics.html(SafeHtml::trusted(render_hero(demo))),
|
||||||
|
ui::control_center::slots::board.html(SafeHtml::trusted(render_board(demo))),
|
||||||
|
ui::control_center::slots::activity.html(SafeHtml::trusted(render_activity(demo))),
|
||||||
|
ui::control_center::slots::inspector.html(SafeHtml::trusted(render_inspector(demo))),
|
||||||
|
ui::control_center::slots::notice.text(notice),
|
||||||
|
ui::control_center::forms::launch_work.clear("title"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_lane(value: Option<&str>) -> usize {
|
||||||
|
let value = value.unwrap_or(LANES[0].0);
|
||||||
|
LANES.iter().position(|(id, _, _)| *id == value).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page_html(demo: &DemoState) -> String {
|
||||||
|
ui::control_center::lower_html(include_str!("../templates/control_center.heml"))
|
||||||
|
.replace("__HERO__", &render_hero(demo))
|
||||||
|
.replace("__BOARD__", &render_board(demo))
|
||||||
|
.replace("__INSPECTOR__", &render_inspector(demo))
|
||||||
|
.replace("__ACTIVITY__", &render_activity(demo))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shell(body: String) -> String {
|
||||||
|
format!(
|
||||||
|
r#"<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>slhx Techdemo</title>
|
||||||
|
<script src="/slhx.js" defer></script>
|
||||||
|
<style>
|
||||||
|
:root {{ color-scheme: dark; --bg:#070814; --panel:rgba(255,255,255,.08); --line:rgba(255,255,255,.16); --text:#f7f7ff; --muted:#aeb3d8; --hot:#ff4fd8; --cyan:#44e7ff; --lime:#b8ff5a; --amber:#ffd166; }}
|
||||||
|
* {{ box-sizing:border-box; }}
|
||||||
|
body {{ margin:0; min-height:100vh; font-family:Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; color:var(--text); background: radial-gradient(circle at top left, rgba(68,231,255,.24), transparent 32rem), radial-gradient(circle at 80% 10%, rgba(255,79,216,.2), transparent 26rem), linear-gradient(135deg, #070814 0%, #111534 55%, #080916 100%); }}
|
||||||
|
body::before {{ content:""; position:fixed; inset:0; pointer-events:none; background-image:linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px); background-size:42px 42px; mask-image:linear-gradient(to bottom, black, transparent); }}
|
||||||
|
main {{ width:min(1180px, calc(100vw - 32px)); margin:0 auto; padding:38px 0 56px; }}
|
||||||
|
.hero-shell {{ display:grid; grid-template-columns:1.35fr .85fr; gap:22px; align-items:stretch; }}
|
||||||
|
.hero-copy, .hero-panel, .command-card, .board-card, .glass-card, .topology {{ border:1px solid var(--line); background:linear-gradient(145deg, rgba(255,255,255,.12), rgba(255,255,255,.05)); box-shadow:0 24px 90px rgba(0,0,0,.32); backdrop-filter: blur(18px); border-radius:28px; }}
|
||||||
|
.hero-copy {{ padding:34px; overflow:hidden; position:relative; }}
|
||||||
|
.hero-copy::after {{ content:"EffectBatch"; position:absolute; right:-18px; bottom:8px; font-size:86px; font-weight:900; color:rgba(255,255,255,.045); }}
|
||||||
|
.eyebrow {{ color:var(--cyan); text-transform:uppercase; letter-spacing:.2em; font-weight:800; font-size:12px; }}
|
||||||
|
h1 {{ font-size:clamp(42px, 7vw, 82px); line-height:.91; letter-spacing:-.07em; margin:12px 0 18px; max-width:850px; }}
|
||||||
|
h2 {{ margin:0 0 16px; letter-spacing:-.035em; }}
|
||||||
|
.lede {{ color:var(--muted); font-size:19px; line-height:1.55; max-width:720px; }}
|
||||||
|
.hero-panel {{ padding:24px; }}
|
||||||
|
.metrics {{ display:grid; gap:14px; }}
|
||||||
|
.metric {{ border:1px solid var(--line); border-radius:22px; padding:18px; background:rgba(0,0,0,.18); }}
|
||||||
|
.metric strong {{ display:block; font-size:34px; line-height:1; }}
|
||||||
|
.metric span {{ color:var(--muted); font-size:13px; }}
|
||||||
|
.topology {{ display:flex; gap:10px; margin:18px 0; padding:10px; }}
|
||||||
|
.topology a {{ color:var(--text); text-decoration:none; padding:12px 16px; border-radius:18px; background:rgba(255,255,255,.08); }}
|
||||||
|
.workspace {{ display:grid; grid-template-columns:360px 1fr; gap:18px; }}
|
||||||
|
.command-card, .board-card, .glass-card {{ padding:22px; }}
|
||||||
|
label {{ display:grid; gap:8px; color:var(--muted); font-size:13px; margin:12px 0; }}
|
||||||
|
input, select, button {{ width:100%; border:1px solid var(--line); border-radius:16px; color:var(--text); background:rgba(0,0,0,.25); padding:13px 14px; font:inherit; }}
|
||||||
|
button {{ cursor:pointer; font-weight:800; background:linear-gradient(135deg, rgba(68,231,255,.25), rgba(255,79,216,.22)); }}
|
||||||
|
button:hover {{ border-color:rgba(68,231,255,.7); transform:translateY(-1px); }}
|
||||||
|
.quick-actions {{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:12px; }}
|
||||||
|
.notice {{ color:var(--lime); min-height:1.4em; }}
|
||||||
|
.section-heading {{ display:flex; justify-content:space-between; gap:12px; color:var(--muted); margin-bottom:14px; }}
|
||||||
|
.section-heading strong {{ color:var(--cyan); }}
|
||||||
|
.lanes {{ display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:14px; }}
|
||||||
|
.lane {{ min-height:330px; border:1px solid var(--line); border-radius:24px; padding:14px; background:rgba(0,0,0,.18); }}
|
||||||
|
.lane h3 {{ margin:0 0 4px; }}
|
||||||
|
.lane p {{ color:var(--muted); margin:0 0 12px; font-size:13px; }}
|
||||||
|
.work-card {{ border:1px solid rgba(255,255,255,.18); border-radius:20px; margin:12px 0; padding:14px; background:linear-gradient(145deg, rgba(255,255,255,.13), rgba(255,255,255,.05)); }}
|
||||||
|
.work-card header {{ display:flex; justify-content:space-between; gap:10px; align-items:start; }}
|
||||||
|
.pill {{ display:inline-flex; border:1px solid var(--line); border-radius:999px; padding:4px 9px; font-size:12px; color:var(--lime); }}
|
||||||
|
.impact {{ height:7px; border-radius:999px; background:rgba(255,255,255,.12); overflow:hidden; margin:12px 0; }}
|
||||||
|
.impact i {{ display:block; height:100%; background:linear-gradient(90deg,var(--cyan),var(--hot)); }}
|
||||||
|
.card-actions {{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; }}
|
||||||
|
.card-actions button {{ padding:9px 10px; font-size:12px; }}
|
||||||
|
.insight-grid {{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:18px; margin-top:18px; }}
|
||||||
|
.glass-card {{ min-height:220px; }}
|
||||||
|
.glow {{ box-shadow:0 0 0 1px rgba(184,255,90,.12), 0 24px 90px rgba(184,255,90,.08); }}
|
||||||
|
.activity {{ display:grid; gap:10px; padding:0; margin:0; list-style:none; }}
|
||||||
|
.activity li, .inspector-row, .live-row {{ border:1px solid var(--line); border-radius:16px; padding:12px; background:rgba(0,0,0,.18); color:var(--muted); }}
|
||||||
|
.inspector-row b {{ color:var(--text); }}
|
||||||
|
.live-row strong {{ color:var(--lime); }}
|
||||||
|
code {{ color:var(--cyan); }}
|
||||||
|
@media (max-width: 920px) {{ .hero-shell,.workspace,.insight-grid {{ grid-template-columns:1fr; }} .lanes {{ grid-template-columns:1fr; }} }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>{body}</body>
|
||||||
|
</html>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_hero(demo: &DemoState) -> String {
|
||||||
|
let shipped = demo.work.iter().filter(|item| item.stage == Stage::Shipped).count();
|
||||||
|
let active = demo.work.iter().filter(|item| item.stage == Stage::Active).count();
|
||||||
|
let impact: u64 = demo.work.iter().map(|item| item.impact as u64).sum();
|
||||||
|
format!(
|
||||||
|
r#"<div class="metrics">
|
||||||
|
<div class="metric"><strong>{}</strong><span>generated resources on this page</span></div>
|
||||||
|
<div class="metric"><strong>{}</strong><span>active typed work items</span></div>
|
||||||
|
<div class="metric"><strong>{}</strong><span>shipped without app JS</span></div>
|
||||||
|
<div class="metric"><strong>{}</strong><span>aggregate impact score</span></div>
|
||||||
|
</div>"#,
|
||||||
|
13,
|
||||||
|
active,
|
||||||
|
shipped,
|
||||||
|
impact,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_board(demo: &DemoState) -> String {
|
||||||
|
let mut out = String::from("<div class=\"lanes\">");
|
||||||
|
for (idx, (_, title, description)) in LANES.iter().enumerate() {
|
||||||
|
out.push_str(&format!(r#"<section class="lane"><h3>{}</h3><p>{}</p>"#, escape_html(title), escape_html(description)));
|
||||||
|
for item in demo.work.iter().filter(|item| item.lane == idx) {
|
||||||
|
out.push_str(&render_card(item));
|
||||||
|
}
|
||||||
|
out.push_str("</section>");
|
||||||
|
}
|
||||||
|
out.push_str("</div>");
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_card(item: &WorkItem) -> String {
|
||||||
|
format!(
|
||||||
|
r#"<article class="work-card" data-key="{id}">
|
||||||
|
<header><strong>{title}</strong><span class="pill">{stage}</span></header>
|
||||||
|
<div class="impact"><i style="width:{impact_percent}%"></i></div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button type="button" data-hid="{spotlight}" data-work-id="{id}">Inspect</button>
|
||||||
|
<button type="button" data-hid="{advance}" data-work-id="{id}">Advance</button>
|
||||||
|
<button type="button" data-hid="{delete}" data-work-id="{id}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</article>"#,
|
||||||
|
id = item.id,
|
||||||
|
title = escape_html(&item.title),
|
||||||
|
stage = item.stage.label(),
|
||||||
|
impact_percent = item.impact as usize * 11,
|
||||||
|
spotlight = ui::control_center::handles::spotlight_work.id().id,
|
||||||
|
advance = ui::control_center::handles::advance_work.id().id,
|
||||||
|
delete = ui::control_center::handles::delete_work.id().id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_activity(demo: &DemoState) -> String {
|
||||||
|
let mut out = String::from("<ol class=\"activity\">");
|
||||||
|
for item in &demo.activity {
|
||||||
|
out.push_str(&format!("<li>{}</li>", escape_html(item)));
|
||||||
|
}
|
||||||
|
out.push_str("</ol>");
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_inspector(demo: &DemoState) -> String {
|
||||||
|
format!(
|
||||||
|
r#"<div class="inspector-row"><b>Selected fact</b><br>{}</div>
|
||||||
|
<div class="inspector-row"><b>Wire contract</b><br><code>POST __h → application/slhx → EffectBatch</code></div>
|
||||||
|
<div class="inspector-row"><b>Runtime</b><br>Root-scoped delegated listeners; numeric targets only.</div>"#,
|
||||||
|
escape_html(&demo.spotlight),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_feed(tick: u64) -> String {
|
||||||
|
format!(
|
||||||
|
r#"<div class="live-row"><strong>SSE tick #{tick}</strong><br>Server streamed a typed EffectBatch into <code>slots::live_feed</code>.</div>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn architecture_hero() -> String {
|
||||||
|
"<div class=\"metrics\"><div class=\"metric\"><strong>1</strong><span>template source of truth</span></div><div class=\"metric\"><strong>0</strong><span>CSS selectors in handlers</span></div><div class=\"metric\"><strong>∞</strong><span>typed composition through tuples</span></div></div>".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn architecture_board() -> String {
|
||||||
|
"<div class=\"lanes\"><section class=\"lane\"><h3>hemplate</h3><p>Owns syntax and Surface facts.</p></section><section class=\"lane\"><h3>slhx-build</h3><p>Generates resources and lowering tables.</p></section><section class=\"lane\"><h3>runtime</h3><p>Executes compact EffectBatch ops.</p></section></div>".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn architecture_inspector() -> String {
|
||||||
|
"<div class=\"inspector-row\"><b>Page swap</b><br>This route was fetched as a partial and rendered through the same root.</div>".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn architecture_activity() -> String {
|
||||||
|
"<ol class=\"activity\"><li>Clicked a real anchor</li><li>Fetched HTML with X-SLHX-Partial</li><li>Preserved native fallback semantics</li></ol>".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_html(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<main data-slhx-root="techdemo" data-slhx-sse="/events">
|
||||||
|
<section class="hero-shell">
|
||||||
|
<div class="hero-copy">
|
||||||
|
<p class="eyebrow">Semantic, Laterally HX</p>
|
||||||
|
<h1>Rust owns the interaction graph. The browser executes tiny typed effects.</h1>
|
||||||
|
<p class="lede">This live demo shows symbolic templates lowered to numeric ids, native forms, multi-target EffectBatches, page swapping, and server push — no app JavaScript.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-panel" data-slhx-slot="hero_metrics">__HERO__</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="topology" data-slhx-slot="nav">
|
||||||
|
<a href="/" data-slhx-nav="">Live system</a>
|
||||||
|
<a href="/architecture" data-slhx-nav="">Architecture</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="workspace">
|
||||||
|
<aside class="command-card">
|
||||||
|
<h2>Launch typed work</h2>
|
||||||
|
<form data-slhx-handle="launch_work" data-slhx-form="launch_work" data-slhx-disable-while-pending>
|
||||||
|
<label>Title <input name="title" required="required" value="Ship typed effects"></label>
|
||||||
|
<label>Lane
|
||||||
|
<select name="lane" required="required">
|
||||||
|
<option value="compiler">Compiler</option>
|
||||||
|
<option value="runtime">Runtime</option>
|
||||||
|
<option value="product">Product</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Impact <input name="impact" type="number" min="1" max="9" value="7"></label>
|
||||||
|
<button type="submit">Launch work</button>
|
||||||
|
</form>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<button type="button" data-slhx-handle="simulate_push">Simulate server push</button>
|
||||||
|
<button type="button" data-slhx-handle="reset_demo">Reset demo</button>
|
||||||
|
</div>
|
||||||
|
<p data-slhx-slot="notice" class="notice">Every control posts a numeric handle id and receives an EffectBatch.</p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="board-card">
|
||||||
|
<div class="section-heading">
|
||||||
|
<span>Generated slots + keyed cards</span>
|
||||||
|
<strong>no selectors, no VDOM</strong>
|
||||||
|
</div>
|
||||||
|
<div data-slhx-slot="board">__BOARD__</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="insight-grid">
|
||||||
|
<article class="glass-card">
|
||||||
|
<h2>Effect inspector</h2>
|
||||||
|
<div data-slhx-slot="inspector">__INSPECTOR__</div>
|
||||||
|
</article>
|
||||||
|
<article class="glass-card">
|
||||||
|
<h2>Activity stream</h2>
|
||||||
|
<div data-slhx-slot="activity">__ACTIVITY__</div>
|
||||||
|
</article>
|
||||||
|
<article class="glass-card glow">
|
||||||
|
<h2>Server push</h2>
|
||||||
|
<div data-slhx-slot="live_feed">Waiting for SSE heartbeat…</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="button" data-slhx-handle="advance_work" hidden="hidden">Advance</button>
|
||||||
|
<button type="button" data-slhx-handle="delete_work" hidden="hidden">Delete</button>
|
||||||
|
<button type="button" data-slhx-handle="spotlight_work" hidden="hidden">Spotlight</button>
|
||||||
|
</main>
|
||||||
Reference in New Issue
Block a user