diff --git a/CLAUDE.md b/CLAUDE.md index d16e96d..6b4fd5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -353,6 +353,28 @@ let (items, navigator) = build_page_view(page, request, RoasterView::from, ROASTER_PAGE_PATH, ROASTER_FRAGMENT_PATH, search); ``` +### When to Use Datastar vs JavaScript + +**Use Datastar for:** +- Visibility toggling (`data-show` + signals) — replaces `classList.add/remove("hidden")` +- List CRUD — delete with `confirm() && @delete()`, create with `@post()` + fragment re-render +- Debounced search — `data-on:input__debounce.300ms` + `@get()` with `responseOverrides` +- AI extraction signal patching — server returns `application/json` signal patches via `render_signals_json()` +- Multi-step wizards — step signals (`$_step`) with `data-show="$_step === N"` +- Searchable selection lists — use `` component with `data-on:change` + +**Use JavaScript for:** +- Browser APIs: WebAuthn (`navigator.credentials`), clipboard (`navigator.clipboard`), geolocation (`navigator.geolocation`), FileReader +- Infinite scroll (`IntersectionObserver` in `base.html`) — no native Datastar equivalent +- Theme toggle — must run in `` before DOM renders, manipulates `` data-theme attribute + `localStorage` +- Any flow that requires `window.location.reload()` after completion (delete passkey, revoke token) + +**Signal naming conventions for in-progress states:** +- `_extracting` — AI extraction in progress (consistent across home, add, check-in pages) +- `_submitting` — form save/create in progress +- `_extract-error` / `_error` — error message signals +- `_show-{thing}` — boolean visibility toggles (e.g. `_show-passkey-form`) + ### Static Assets Static files live in `static/` and are compiled into the binary via `include_str!()`/`include_bytes!()`. Each file needs an explicit route in `application/routes/mod.rs`: diff --git a/src/application/routes/support.rs b/src/application/routes/support.rs index 337e347..ae0bf16 100644 --- a/src/application/routes/support.rs +++ b/src/application/routes/support.rs @@ -279,35 +279,6 @@ pub fn set_datastar_patch_headers(headers: &mut HeaderMap, selector: &'static st let _ = headers.insert("datastar-mode", HeaderValue::from_static("replace")); } -/// Render a `
` fragment with `data-signals` attributes for Datastar signal merging. -/// -/// The selector must be a `#id` selector. Signal values are JSON-encoded and HTML-escaped -/// so they are safe to embed in HTML attributes and evaluate as JavaScript expressions. -pub fn render_signals_fragment( - selector: &'static str, - signals: &[(&str, serde_json::Value)], -) -> Result { - use std::fmt::Write; - - let id = selector.strip_prefix('#').unwrap_or(selector); - - let mut html = format!(r#"
"); - - let mut response = Html(html).into_response(); - response - .headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static("text/html")); - set_datastar_patch_headers(response.headers_mut(), selector); - Ok(response) -} - /// Return a JSON response that Datastar interprets as a signal patch. /// /// Signal names may use kebab-case (`_roaster-name`); they are automatically @@ -341,13 +312,6 @@ fn kebab_to_camel(s: &str) -> String { result } -fn escape_html_attr(s: &str) -> String { - s.replace('&', "&") - .replace('"', """) - .replace('<', "<") - .replace('>', ">") -} - #[cfg(test)] mod tests { use super::*; diff --git a/templates/pages/account.html b/templates/pages/account.html index 96a7618..e4eb2d4 100644 --- a/templates/pages/account.html +++ b/templates/pages/account.html @@ -5,7 +5,7 @@ {% endblock %} {% block content %} -
+

Passkeys

@@ -43,7 +43,8 @@ {% endif %} -
-
+

API Tokens

{% if tokens.is_empty() %} @@ -125,7 +126,8 @@ {% endif %} - {% endif %}
@@ -219,13 +202,13 @@ const filterList = (input, listId) => {

What are you drinking?

-

Scan a bag to identify the coffee, or select from your existing roasts below.

+

Scan a bag to identify the coffee, or select from your existing roasts below.

- {% call scan::scan_input("checkin-scan-form", "checkin-image", "$_scanWaiting", "$_scanError", "Describe the coffee…", "Scanning…") %} + {% call scan::scan_input("checkin-scan-form", "checkin-image", "$_extracting", "$_extractError", "Describe the coffee…", "Scanning…") %}
@@ -240,26 +223,18 @@ const filterList = (input, listId) => { {% if !roast_options.is_empty() %}

Or select an existing roast:

- - +
{% endif %} diff --git a/templates/pages/data.html b/templates/pages/data.html index 8bfd864..f2058ac 100644 --- a/templates/pages/data.html +++ b/templates/pages/data.html @@ -19,14 +19,14 @@
{% include "partials/tab_bar.html" %} -
+
@@ -36,30 +36,4 @@
- - {% endblock %}