refactor(techdemo): scope card handles to partials

Move board card and lane actions onto their reusable partial templates, register the scoped generated handles directly, and remove hidden shell-level handle anchors and manual handle fields from the rendered view data.

req: component/003

req: view/001

req: examples/001
This commit is contained in:
slhx agent
2026-06-01 21:58:40 +02:00
parent c69eb9932c
commit 4319dea171
7 changed files with 22 additions and 33 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ mod tests {
fn techdemo_exports_form_and_interaction_handles() {
assert_ne!(
ui::control_center::handles::launch_work.id(),
ui::control_center::handles::advance_work.id()
ui::issue_card::handles::advance_work.id()
);
assert_eq!(
ui::control_center::forms::launch_work.field("title").resource,
+8 -15
View File
@@ -4,7 +4,7 @@ use axum::routing::get;
use axum::Router;
use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{CssClass, CssClasses, Handle, IntoEffect, RenderSlotExt, SafeHtml};
use slhx::{CssClass, CssClasses, IntoEffect, RenderSlotExt, SafeHtml};
use slhx_axum::{
runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm,
PageRequest,
@@ -102,7 +102,6 @@ struct Shared {
struct IssueLane {
class: CssClass,
lane_id: &'static str,
move_handle: Handle<()>,
title: &'static str,
description: &'static str,
cards: Vec<IssueCard>,
@@ -116,9 +115,6 @@ struct IssueCard {
title: String,
stage: &'static str,
impact_style: String,
spotlight_handle: Handle<()>,
advance_handle: Handle<()>,
delete_handle: Handle<()>,
}
#[derive(Hemplate)]
@@ -304,7 +300,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Launch accepted · 4 targets updated")
}
})
.register_handle(ui::control_center::handles::advance_work, {
.register_handle(ui::issue_card::handles::advance_work, {
let shared = shared.clone();
move |form| {
// req: list/003 req: examples/001
@@ -322,7 +318,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Pipeline advanced")
}
})
.register_handle(ui::control_center::handles::move_to_lane, {
.register_handle(ui::issue_lane::handles::move_to_lane, {
let shared = shared.clone();
move |form| {
// req: list/003 req: examples/001
@@ -344,7 +340,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Drag-and-drop move persisted")
}
})
.register_handle(ui::control_center::handles::delete_work, {
.register_handle(ui::issue_card::handles::delete_work, {
let shared = shared.clone();
move |form| {
// req: list/003 req: examples/001
@@ -363,7 +359,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Card removed")
}
})
.register_handle(ui::control_center::handles::spotlight_work, {
.register_handle(ui::issue_card::handles::spotlight_work, {
let shared = shared.clone();
move |form| {
// req: examples/001
@@ -416,7 +412,7 @@ fn update_work(demo: &mut DemoState, id: Option<&str>, update: impl FnOnce(&mut
fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect {
(
ui::control_center::slots::hero_metrics.render(&hero_view(demo)),
ui::control_center::slots::board.render(&board_view(demo)),
ui::control_center::slots::board.html(render_board(demo)),
ui::control_center::slots::activity.render(&activity_view(demo)),
ui::control_center::slots::inspector.render(&inspector_view(demo)),
ui::control_center::slots::notice.text(notice),
@@ -491,7 +487,6 @@ fn board_view(demo: &DemoState) -> BoardLanes {
.map(|(idx, (lane_id, title, description))| IssueLane {
class: ui::control_center::classes::lane,
lane_id,
move_handle: ui::control_center::handles::move_to_lane,
title,
description,
cards: demo
@@ -508,7 +503,8 @@ fn board_view(demo: &DemoState) -> BoardLanes {
fn render_board(demo: &DemoState) -> SafeHtml {
// req: html_safety/002 req: view/001
slhx::render_html(&board_view(demo))
let html = render_template(&board_view(demo));
SafeHtml::trusted(ui::issue_lane::lower_html(ui::issue_card::lower_html(html)))
}
fn issue_card(item: &WorkItem, selected: bool) -> IssueCard {
@@ -525,9 +521,6 @@ fn issue_card(item: &WorkItem, selected: bool) -> IssueCard {
title: item.title.clone(),
stage: item.stage.label(),
impact_style: format!("width:{}%", item.impact as usize * 11),
spotlight_handle: ui::control_center::handles::spotlight_work,
advance_handle: ui::control_center::handles::advance_work,
delete_handle: ui::control_center::handles::delete_work,
}
}
@@ -65,8 +65,4 @@
</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>
<button type="button" data-slhx-handle="move_to_lane" hidden="hidden">Move to lane</button>
</main>
@@ -2,8 +2,8 @@
<header><strong>{+ self.title +}</strong><span class="pill">{+ self.stage +}</span></header>
<div class="impact"><i +style="self.impact_style"></i></div>
<div class="card-actions">
<button type="button" +data-hid="self.spotlight_handle" +data-work-id="self.id">Inspect</button>
<button type="button" +data-hid="self.advance_handle" +data-work-id="self.id">Advance</button>
<button type="button" +data-hid="self.delete_handle" +data-work-id="self.id">Delete</button>
<button type="button" data-slhx-handle="spotlight_work" +data-work-id="self.id">Inspect</button>
<button type="button" data-slhx-handle="advance_work" +data-work-id="self.id">Advance</button>
<button type="button" data-slhx-handle="delete_work" +data-work-id="self.id">Delete</button>
</div>
</article>
@@ -1,4 +1,4 @@
<section +class="self.class" +data-lane="self.lane_id" +data-hid="self.move_handle" data-slhx-on="drop">
<section +class="self.class" +data-lane="self.lane_id" data-slhx-handle="move_to_lane" data-slhx-on="drop">
<h3>{+ self.title +}</h3>
<p>{+ self.description +}</p>
<template h-for="card in &self.cards">
+2 -2
View File
@@ -72,7 +72,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice), "Drag-and-drop move persisted").await?;
driver
.find(By::Css(&card_button_selector(ui::control_center::handles::spotlight_work, 2)))
.find(By::Css(&card_button_selector(ui::issue_card::handles::spotlight_work, 2)))
.await?
.click()
.await?;
@@ -84,7 +84,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
.await?;
driver
.find(By::Css(&card_button_selector(ui::control_center::handles::advance_work, 3)))
.find(By::Css(&card_button_selector(ui::issue_card::handles::advance_work, 3)))
.await?
.click()
.await?;
+7 -7
View File
@@ -116,32 +116,32 @@ fn product_is_e2e_working_over_http() {
let move_to_lane = post(
"/",
&format!("__h={}&work_id=4&lane=runtime", ui::control_center::handles::move_to_lane),
&format!("__h={}&work_id=4&lane=runtime", ui::issue_lane::handles::move_to_lane),
);
assert_effect_response(&move_to_lane);
let move_to_lane_batch = move_to_lane.batch();
assert_card(&move_to_lane_batch, "Design hero moment", "Runtime", "Active", "width:99%");
assert_payload_contains(&move_to_lane_batch, "Drag-and-drop move persisted");
let inspect = post("/", "__h=3112123592&work_id=4");
let inspect = post("/", &format!("__h={}&work_id=4", ui::issue_card::handles::spotlight_work));
assert_effect_response(&inspect);
let inspect_batch = inspect.batch();
assert_payload_contains(&inspect_batch, "Design hero moment · lane=Runtime");
assert_payload_contains(&inspect_batch, "Inspector focused");
let advance = post("/", "__h=4085043678&work_id=4");
let advance = post("/", &format!("__h={}&work_id=4", ui::issue_card::handles::advance_work));
assert_effect_response(&advance);
let advance_batch = advance.batch();
assert_payload_contains(&advance_batch, "Pipeline advanced");
assert_payload_contains(&advance_batch, "<span class=\"pill\">Active</span>");
let advance_default = post("/", "__h=4085043678&work_id=5");
let advance_default = post("/", &format!("__h={}&work_id=5", ui::issue_card::handles::advance_work));
assert_effect_response(&advance_default);
let advance_default_batch = advance_default.batch();
assert_payload_contains(&advance_default_batch, "Default impact");
assert_card(&advance_default_batch, "Default impact", "Compiler", "Active", "width:55%");
let ship_default = post("/", "__h=4085043678&work_id=5");
let ship_default = post("/", &format!("__h={}&work_id=5", ui::issue_card::handles::advance_work));
assert_effect_response(&ship_default);
let ship_default_batch = ship_default.batch();
assert_card(&ship_default_batch, "Default impact", "Product", "Shipped", "width:55%");
@@ -154,12 +154,12 @@ fn product_is_e2e_working_over_http() {
assert_payload_contains(&push_batch, "Simulated push event produced the same EffectBatch shape");
assert_emit(&push_batch, "slhx:island-orbit", "activity rows");
let delete_missing = post("/", "__h=165211725&work_id=999");
let delete_missing = post("/", &format!("__h={}&work_id=999", ui::issue_card::handles::delete_work));
assert_effect_response(&delete_missing);
let delete_missing_batch = delete_missing.batch();
assert_payload_not_contains(&delete_missing_batch, "Deleted card #999");
let delete = post("/", "__h=165211725&work_id=4");
let delete = post("/", &format!("__h={}&work_id=4", ui::issue_card::handles::delete_work));
assert_effect_response(&delete);
let delete_batch = delete.batch();
assert_payload_contains(&delete_batch, "Card removed");