diff --git a/examples/html_examples/README.md b/examples/html_examples/README.md index fe6ff2f..ee631bf 100644 --- a/examples/html_examples/README.md +++ b/examples/html_examples/README.md @@ -51,7 +51,7 @@ Status legend: | `modal-custom` | deferred | A custom modal can be a generated partial plus focus/escape policy, but needs accessibility proof before copy/paste. | Later slice should include keyboard/focus tests. | | `tabs-hateoas` | deferred | Good hemx fit: server-owned selected tab and generated tab panel replacement; needs a focused slice. | Later slice should add one tab group. | | `tabs-javascript` | refused | Client-owned tab state is exactly what generated server-owned state is meant to avoid unless a product needs it. | Prefer `tabs-hateoas`. | -| `keyboard-shortcuts` | integration-owned | Keyboard policy belongs to the app/host; hemx should only receive explicit events. | Use `Effect::event`/app JS when needed. | +| `keyboard-shortcuts` | integration-owned | Keyboard policy belongs to the app/host; hemx should only receive explicit events. | Use generated app-level events / app JS when needed. | | `sortable` | integration-owned | Drag/drop ordering needs a browser library or pointer policy plus server reorder command. | Keep Sortable.js as app-owned integration until proven reusable. | | `update-other-content` | implemented | Generated effects can update multiple slots from one handler; validation and search already update status plus rows/errors. | `validate_email`, `search` handlers. | | `confirm` | integration-owned | Confirmation wording and irreversible-action policy belong to the app; hemx should not own a global confirm system. | Use native confirm/app dialog around generated delete forms. | diff --git a/examples/html_examples/src/main.rs b/examples/html_examples/src/main.rs index 26765b7..8875689 100644 --- a/examples/html_examples/src/main.rs +++ b/examples/html_examples/src/main.rs @@ -1,9 +1,9 @@ +use axum::body::Body; use axum::extract::State; -use axum::response::IntoResponse; +use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::Router; use hemplate::Hemplate; -use hemx::advanced::Effect; use hemx::{Html, IntoEffect}; use hemx_axum::{ interactions, runtime_js, runtime_js_path, EffectResponse, Form, InteractionHandlers, @@ -227,6 +227,7 @@ async fn main() { let app = Router::new() .route("/", get(home).post(interact)) .route(runtime_js_path(), get(runtime)) + .route("/app.css", get(css)) .with_state(state); let addr = SocketAddr::from(([127, 0, 0, 1], 3029)); @@ -288,6 +289,13 @@ async fn runtime() -> impl IntoResponse { runtime_js() } +async fn css() -> Response { + Response::builder() + .header("content-type", "text/css; charset=utf-8") + .body(Body::from(include_str!("../templates/app.css"))) + .expect("css response") +} + #[hemx::app(gallery_handlers, contact_card_handlers, editable_row_handlers)] fn handlers(state: Arc) -> InteractionHandlers { interactions(ui::BUILD_FINGERPRINT) @@ -459,7 +467,7 @@ mod gallery_handlers { value_options_for(&category, &value) .into_iter() .map(|option| gallery::value_option.replace(option)) - .collect::>() + .collect::>() } #[hemx::handler] @@ -490,7 +498,7 @@ mod gallery_handlers { let rows = loaded_rows(*count) .into_iter() .map(|row| gallery::infinite_row.replace(row)) - .collect::>(); + .collect::>(); let mut effects = rows; effects.push(gallery::infinite_status.set(format!("Showing {} rows", *count))); effects @@ -530,7 +538,7 @@ mod gallery_handlers { let rows = loaded_rows(*loaded) .into_iter() .map(|row| gallery::loaded_row.replace(row)) - .collect::>(); + .collect::>(); let mut effects = rows; effects.push(gallery::load_status.set(format!("Showing {} rows", *loaded))); effects @@ -546,7 +554,7 @@ mod gallery_handlers { let results = search_results_for(&query) .into_iter() .map(|result| gallery::search_result.replace(result)) - .collect::>(); + .collect::>(); let mut effects = results; effects.push(gallery::search_status.set(if query.is_empty() { "Showing all results".into() diff --git a/examples/html_examples/templates/app.css b/examples/html_examples/templates/app.css new file mode 100644 index 0000000..a52f706 --- /dev/null +++ b/examples/html_examples/templates/app.css @@ -0,0 +1,124 @@ +:root { + --bg: #f5f5f5; + --surface: #ffffff; + --ink: #222222; + --muted: #666666; + --border: #d4d4d4; + --accent: #3465a4; + --accent-hover: #29528a; + --danger: #c0392b; + --danger-hover: #a93226; + --radius: 6px; + --space: 1.25rem; + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; +} +* { box-sizing: border-box; } +body { + margin: 0; + padding: var(--space); + font-family: var(--font-body); + background: var(--bg); + color: var(--ink); + line-height: 1.55; +} +main { + max-width: 820px; + margin: 0 auto; +} +header { + margin-bottom: calc(var(--space) * 1.5); +} +header p:first-child { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.75rem; + color: var(--muted); + margin: 0 0 0.25rem; +} +h1 { + font-family: var(--font-mono); + font-size: 1.75rem; + margin: 0 0 0.5rem; +} +header p:last-child { + color: var(--muted); + margin: 0; +} +section { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: calc(var(--space) * 1.25); + margin-bottom: var(--space); +} +section h2 { + font-family: var(--font-mono); + font-size: 1.15rem; + margin: 0 0 var(--space); + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} +p { margin: 0 0 var(--space); } +form { margin: 0 0 var(--space); } +label { font-weight: 500; } +article label { display: block; margin-bottom: 0.75rem; } +article label input { display: block; width: 100%; margin-top: 0.3rem; } +input, select, button { + font: inherit; + padding: 0.45rem 0.65rem; + border-radius: var(--radius); + border: 1px solid var(--border); +} +input, select { width: 100%; max-width: 360px; } +input:focus, select:focus, button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +button { + background: var(--accent); + color: #fff; + border-color: var(--accent); + cursor: pointer; + font-weight: 500; +} +button:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); +} +[data-hemx-handle="delete_row"] button, +[data-hemx-handle*="delete"] button, +.danger { + background: var(--danger); + border-color: var(--danger); +} +[data-hemx-handle="delete_row"] button:hover, +[data-hemx-handle*="delete"] button:hover, +.danger:hover { + background: var(--danger-hover); + border-color: var(--danger-hover); +} +td form { display: inline-block; margin-right: 0.4rem; } +table { + width: 100%; + border-collapse: collapse; + margin: var(--space) 0; +} +th, td { + text-align: left; + padding: 0.5rem; + border-bottom: 1px solid var(--border); +} +th { font-weight: 600; color: var(--muted); } +ul { padding-left: 1.25rem; margin: 0 0 var(--space); } +progress { + width: 100%; + height: 1rem; + accent-color: var(--accent); +} +[data-hemx-error-for] { + color: var(--danger); + font-size: 0.9rem; + margin: 0.25rem 0 0; +} +.htmx-indicator { opacity: 0.5; } diff --git a/examples/html_examples/templates/app_shell.heml b/examples/html_examples/templates/app_shell.heml index 4f13f59..903cae8 100644 --- a/examples/html_examples/templates/app_shell.heml +++ b/examples/html_examples/templates/app_shell.heml @@ -4,7 +4,8 @@ hemx HTML examples - + + {+= self.body =+}