polish(techdemo): make demo customer-ready

req: examples/001

req: dx/008

req: form/002
This commit is contained in:
slhx agent
2026-05-11 07:38:29 +02:00
parent e840802721
commit 5a29161148
5 changed files with 52 additions and 41 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ This is a polished Linear-style product demo for planning typed work across lane
- 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
- one tiny launch glue function plus slhx runtime-driven interactions
Verification:
+31 -12
View File
@@ -292,18 +292,35 @@ fn shell(body: String) -> String {
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>slhx Techdemo</title>
<script src="/slhx.js" defer></script>
<script>
function slhxTechdemoLaunch(button) {{
const form = button.closest('form');
const root = button.closest('[data-slhx-root]');
const data = new URLSearchParams(new FormData(form));
data.set('__h', button.getAttribute('data-hid'));
fetch('/', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Accept': 'application/slhx' }},
body: data
}})
.then((response) => response.arrayBuffer())
.then((buffer) => window.slhx.applyBatch(buffer, root));
return false;
}}
</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%); }}
* {{ box-sizing:border-box; min-width:0; }}
html {{ font-feature-settings:"cv02","cv03","cv04","ss01"; text-rendering:geometricPrecision; }}
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 12% 8%, rgba(68,231,255,.28), transparent 28rem), radial-gradient(circle at 82% 4%, rgba(255,79,216,.22), transparent 24rem), radial-gradient(circle at 70% 70%, rgba(184,255,90,.08), transparent 30rem), linear-gradient(135deg, #070814 0%, #111534 55%, #080916 100%); overflow-x:hidden; }}
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, .hero-panel, .command-card, .board-card, .glass-card, .topology {{ border:1px solid var(--line); background:linear-gradient(145deg, rgba(255,255,255,.14), rgba(255,255,255,.055)); box-shadow:0 24px 90px rgba(0,0,0,.36), inset 0 1px 0 rgba(255,255,255,.12); backdrop-filter: blur(22px) saturate(145%); 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; }}
h1 {{ font-size:clamp(42px, 7vw, 84px); line-height:.88; letter-spacing:-.075em; margin:12px 0 18px; max-width:900px; text-wrap:balance; overflow-wrap:anywhere; }}
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; }}
@@ -316,29 +333,31 @@ fn shell(body: String) -> String {
.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; }}
input, select, button {{ width:100%; border:1px solid var(--line); border-radius:16px; color:var(--text); background:rgba(2,4,18,.55); padding:13px 14px; font:inherit; outline:none; transition:transform .16s ease, border-color .16s ease, background .16s ease, box-shadow .16s ease; }}
input:focus, select:focus {{ border-color:rgba(68,231,255,.75); box-shadow:0 0 0 4px rgba(68,231,255,.11); }}
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); }}
.primary-action {{ background:linear-gradient(135deg, var(--cyan), var(--hot)); color:#050610; border:0; box-shadow:0 16px 42px rgba(68,231,255,.24); }}
button:hover {{ border-color:rgba(68,231,255,.7); transform:translateY(-1px); box-shadow:0 14px 40px rgba(0,0,0,.24); }}
.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); }}
.lanes {{ display:grid; grid-template-columns:repeat(3, minmax(220px, 1fr)); gap:14px; align-items:start; }}
.lane {{ min-height:330px; border:1px solid var(--line); border-radius:24px; padding:14px; background:linear-gradient(180deg, rgba(0,0,0,.26), rgba(255,255,255,.035)); overflow:hidden; }}
.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 {{ border:1px solid rgba(255,255,255,.18); border-radius:20px; margin:12px 0; padding:14px; background:linear-gradient(145deg, rgba(255,255,255,.15), rgba(255,255,255,.055)); box-shadow:0 14px 38px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.1); overflow:hidden; overflow-wrap:anywhere; }}
.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; }}
.card-actions {{ display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:8px; }}
.card-actions button {{ padding:9px 10px; font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }}
.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); }}
.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); overflow-wrap:anywhere; }}
.inspector-row b {{ color:var(--text); }}
.live-row strong {{ color:var(--lime); }}
code {{ color:var(--cyan); }}
@@ -1,9 +1,9 @@
<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>
<p class="eyebrow">slhx Control Plane</p>
<h1>A Linear-class work system without a frontend framework.</h1>
<p class="lede">Create, inspect, advance, and stream work through native HTML, generated typed resources, and compact EffectBatches. The UI feels app-grade; the model stays server-owned and boring.</p>
</div>
<div class="hero-panel" data-slhx-slot="hero_metrics">__HERO__</div>
</section>
@@ -15,8 +15,8 @@
<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>
<h2>Create an issue</h2>
<form 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">
@@ -26,19 +26,20 @@
</select>
</label>
<label>Impact <input name="impact" type="number" min="1" max="9" value="7"></label>
<button type="submit">Launch work</button>
<button class="primary-action" type="button" data-slhx-handle="launch_work" onclick="return slhxTechdemoLaunch(this)">Create issue</button>
</form>
<form hidden="hidden" data-slhx-handle="launch_work" data-slhx-form="launch_work"></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>
<p data-slhx-slot="notice" class="notice">Every control posts a numeric handle id and receives typed effects.</p>
</aside>
<section class="board-card">
<div class="section-heading">
<span>Generated slots + keyed cards</span>
<strong>no selectors, no VDOM</strong>
<strong>generated resources, no selectors</strong>
</div>
<div data-slhx-slot="board">__BOARD__</div>
</section>
+8 -18
View File
@@ -47,7 +47,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
let result = async {
driver.goto(&format!("http://{APP_ADDR}/")).await?;
assert_text(&driver, "Rust owns the interaction graph").await?;
assert_text(&driver, "A Linear-class work system without a frontend framework").await?;
assert_text(&driver, "Compile checked handles").await?;
assert_text(&driver, "No selectors. Generated resources address every target.").await?;
wait_for_runtime(&driver).await?;
@@ -70,25 +70,15 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
)
.await?;
driver
.execute(
&format!(
r#"
return fetch("/", {{
method: "POST",
headers: {{ "Content-Type": "application/x-www-form-urlencoded" }},
body: "__h={}&title=Browser+verified+issue&lane=product&impact=8"
}})
.then((response) => response.arrayBuffer())
.then((buffer) => {{ window.slhx.applyBatch(buffer, document.querySelector("[data-slhx-root]")); return true; }});
"#,
ui::control_center::handles::launch_work.id().id
),
Vec::new(),
)
.find(By::Css(&format!(
"button{}",
handle_selector(ui::control_center::handles::launch_work.id().id)
)))
.await?
.click()
.await?;
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Launch accepted").await?;
assert_text(&driver, "Browser verified issue").await?;
wait_for_text(&driver, "body", "Browser verified issue").await?;
assert_text(&driver, "width:88%").await?;
driver
+3 -2
View File
@@ -1,5 +1,6 @@
use scraper::{Html, Selector};
use slhx::{Effect, EffectBatch, Payload, EFFECT_BATCH_ABI_VERSION};
use slhx_techdemo::ui;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
@@ -47,7 +48,7 @@ fn product_is_e2e_working_over_http() {
assert_eq!(home.status, 200);
assert!(home.header("content-type").contains("text/html"));
let document = Html::parse_document(home.text());
assert_text(&document, "Rust owns the interaction graph");
assert_text(&document, "A Linear-class work system without a frontend framework");
assert_text(&document, "Compile checked handles");
assert_text(&document, "Stream typed presence");
assert_selector_count_at_least(&document, "[data-slhx-root=techdemo]", 1);
@@ -163,7 +164,7 @@ fn product_is_e2e_working_over_http() {
fn assert_effect_response(response: &Response) {
assert_eq!(response.status, 200);
assert!(response.header("content-type").contains("application/slhx"));
assert_eq!(response.header("x-slhx-fingerprint"), "9481407110904740166");
assert_eq!(response.header("x-slhx-fingerprint"), ui::BUILD_FINGERPRINT.0.to_string());
let batch = response.batch();
assert_eq!(batch.abi_version, EFFECT_BATCH_ABI_VERSION);
assert!(!batch.ops.is_empty());