diff --git a/CLAUDE.md b/CLAUDE.md index afe6205..66e55e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -223,34 +223,52 @@ Templates use Datastar attributes for interactivity: ```html -
+
- + +
-
+
- - -
``` Key attributes: -- `data-signals:_name="value"` - Local signals (underscore prefix excludes from backend requests) -- `data-show="$_signal"` - Conditional visibility -- `data-on:event="expression"` - Event handlers -- `data-ref="_name"` - DOM element references (underscore prefix for local refs) -- `@get/@post/@put/@delete(url, options)` - HTTP actions with automatic Datastar headers +- `data-signals:_name="value"` — Local signals (underscore prefix excludes from backend requests) +- `data-show="$_signal"` — Conditional visibility +- `data-bind:_signal-name` — Two-way binding between signal and input value +- `data-on:event="expression"` — Event handlers +- `data-ref="_name"` — DOM element references (underscore prefix for local refs) +- `data-text="$_signal"` — Set element text content from signal +- `data-attr:value="$_signal"` — Set element attribute from signal (used for hidden inputs) +- `@get/@post/@put/@delete(url, options)` — HTTP actions with automatic Datastar headers + +#### Signal Naming + +Signal names use kebab-case in HTML attributes and auto-convert to camelCase in JS expressions: + +- HTML attribute: `data-signals:_roaster-name="''"` or `data-bind:_roaster-name` +- JS expression: `$_roasterName` +- JSON response key: `_roasterName` (camelCase) + +#### Two-Way Binding + +Use `data-bind:_signal-name` for two-way binding between signals and form inputs. **Do not use `data-model`** — it does not exist in Datastar v1 and is silently ignored. #### URL Generation @@ -341,55 +359,110 @@ Static files live in `templates/` and are compiled into the binary via `include_ ```rust .route("/styles.css", get(styles)) -.route("/extract.js", get(extract_js)) .route("/favicon.ico", get(favicon)) -async fn extract_js() -> impl IntoResponse { +async fn styles() -> impl IntoResponse { ( - [("content-type", "application/javascript; charset=utf-8")], - include_str!("../../../templates/extract.js"), + [("content-type", "text/css; charset=utf-8")], + include_str!("../../../templates/styles.css"), ) } ``` -There is no `tower-http` static file serving — all assets are embedded at compile time. +There is no `tower-http` static file serving — all assets are embedded at compile time. There are no separate JavaScript files; all interactivity is handled via Datastar attributes and minimal inline JS. -### AI Extraction Controls +### AI Extraction (Datastar-native) -Pages with AI-powered form filling (roasters, roasts, scan) share a common JavaScript library at `templates/extract.js` served at `/extract.js`. It provides three functions: +Pages with AI-powered form filling (roasters, roasts, home page scan) use a fully Datastar-native pattern with **no external JavaScript files**. Extraction endpoints return JSON signal patches that Datastar merges into the signal store, and `data-bind` pushes updated values into form fields automatically. -- `triggerPhotoExtract(formId, endpoint, onSuccess)` — opens camera/file picker, reads as data URL -- `extractFromText(formId, endpoint, onSuccess)` — reads text from input field -- `doExtract(formId, endpoint, body, onSuccess)` — POST to API, toggle waiting state, call callback on success +#### Server Response Format -Each page provides only its own `onSuccess` callback (e.g., `fillRoasterForm`, `fillRoastForm`, `fillScanForms`). +Extraction endpoints detect Datastar requests and return `application/json` signal patches using `render_signals_json()`: -#### Element ID Convention +```rust +// application/routes/support.rs +pub fn render_signals_json( + signals: &[(&str, serde_json::Value)], +) -> Result { ... } -The shared library locates DOM elements using the `formId` prefix: - -| Element | ID pattern | Purpose | -|---------|-----------|---------| -| Controls wrapper | `{formId}-extract-controls` | Hidden during extraction | -| Waiting message | `{formId}-extract-waiting` | Shown during extraction (spinner + text) | -| Error paragraph | `{formId}-extract-error` | Shown on failure | -| Text input | `{formId}-extract-text` | Text description input | - -Example `formId` values: `roaster-form`, `roast-form`, `scan`. - -#### Template Structure - -```html -
- -
- - +// Usage in a handler: +let signals = vec![ + ("_roaster-name", Value::String(result.name)), + ("_roaster-country", Value::String(result.country)), +]; +render_signals_json(&signals) ``` -Buttons wire up via onclick with the callback: `onclick="triggerPhotoExtract('roast-form', '/api/v1/extract-roast', fillRoastForm)"`. +Signal keys use kebab-case (e.g., `_roaster-name`); the function converts them to camelCase (`_roasterName`) for the JSON response. Datastar processes `application/json` responses as signal patches automatically. + +**Important**: Do not return `text/html` fragments with `data-signals` attributes for signal patching — Datastar only processes signal updates from `application/json` responses, not from DOM-patched HTML elements. + +#### `datastar-fetch` Event Bubbling + +The `datastar-fetch` custom event **bubbles through the DOM**. When a page has multiple forms with `data-on:datastar-fetch` handlers (e.g., an extraction form and a create/save form), each handler will fire for events from *any* `@post`/`@get` in the same DOM tree. + +**Every `data-on:datastar-fetch` handler must guard with its own "in progress" signal** to ignore events from other forms: + +```html + + + + + +``` + +Without these guards, the create form's handler will react to the extraction form's `finished` event and vice versa. Note that `evt.detail.type` fires for `started`, `finished`, and `error` — only reset state on `finished` or `error`, never unconditionally. + +#### Extraction Template Pattern + +Each extraction-enabled page uses this structure: + +```html +
+ {% if has_ai_extract %} + + + + + +
+ + + +
+
+

+ + {% endif %} + + +
+ + +
+
+``` + +The only inline JS is the `onchange` handler for FileReader (reading photos as data URLs) and `onclick` to trigger the hidden file input. Everything else is pure Datastar. ### Foursquare Integration