1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//! Server-rendered widget helpers: display primitives and interactive search.
//!
//! All widgets return [`maud::Markup`] and are composable — pass the output of
//! one as the body of another. Every widget is CSP-safe (no inline JS) and
//! HTML-escapes caller-supplied text via Maud.
//!
//! # Features
//!
//! The core widgets (layout, navigation, feedback, data display) need the
//! `maud` feature. Four families have their own feature (issue #3408). Each
//! one is on by default and turns on `maud`.
//!
//! | Feature | Widgets |
//! |---------|---------|
//! | `widgets-search` | `active_search`, `autocomplete_input` (single or tags mode) |
//! | `widgets-social` | `reaction_controls`, `comment_thread`, `infinite_feed` |
//! | `widgets-workflow` | `transition_controls`, `bulk_actions_form` |
//! | `widgets-charts` | `sparkline`, `bar_chart`, `line_chart` |
//!
//! With `default-features = false`, name each family the app uses. A family
//! also controls its CSS in `autumn-widgets.css` and its script in
//! `autumn-widgets.js`.
//!
//! # Display widgets
//!
//! | Widget | Use |
//! |--------|-----|
//! | `card` | Titled content panel with optional header action and footer |
//! | `stat_card` | Metric tile: label / value / optional link |
//! | `property_list` | `<dl>` of label/value rows for a record detail view |
//! | `data_table` | Column-driven, sortable `<table>` |
//! | `breadcrumb` | Accessible `<nav>` breadcrumb trail |
//! | `hero` | Landing-page banner: headline, optional subtitle, and CTAs |
//! | `nav_link` | Navigation anchor, auto-marked active + `aria-current` |
//! | `nav_bar` | Top-bar/sidebar `<nav>` landmark: brand, links, dropdowns, responsive toggle |
//! | `tabs` | No-JS `tablist`/`tab`/`tabpanel` switcher with `:target` deep-linking |
//! | `accordion` / `collapsible` | No-JS `<details>` disclosure. `accordion` is exclusive or multi-open |
//! | `infinite_feed` | htmx infinite-scroll / "Load more" feed from a `CursorPage` |
//! | `reaction_controls` | No-JS vote/like toggle buttons + live aggregate for `#[votable]` |
//! | `comment_thread` | No-JS/htmx nested comment list with an inline reply form per node, for `#[commentable]` |
//! | `locale_switcher` | Path-preserving language switcher for locale-prefixed routing (issue #1251) |
//! | `theme_toggle` | No-JS System / Light / Dark switch that stores the `autumn.theme` cookie (issue #3396) |
//! | `button` / `button_group` | Button with a variant, a size, an icon and a loading state. A `role="group"` of buttons (issue #3406) |
//! | `separator` / `kbd` / `kbd_combo` | `<hr>` or a vertical `role="separator"`. Keyboard keys (issue #3406) |
//! | `avatar_group` | Avatars that overlap, with a `+N` item (issue #3406) |
//! | `progress` / `meter` | Native `<progress>` and `<meter>`, with a name and an optional visible label (issue #3406) |
//!
//! # Feedback widgets
//!
//! | Widget | Use |
//! |--------|-----|
//! | `toast` / `toast_with` / `toast_region` | htmx action feedback, appended out-of-band. Pauses on hover or focus. Has a close button |
//! | `tooltip` / `tooltip_with` | Text that shows on hover and focus, linked by `aria-describedby` (issue #3404) |
//! | `spinner` / `skeleton` | A `role="status"` spinner and a placeholder shape. Each can be an htmx `hx-indicator` (issue #3406) |
//! | `empty_state` | Title, text and action for a list with no data (issue #3406) |
//!
//! # Interactive / search widgets
//!
//! | Situation | Use |
//! |-----------|-----|
//! | Keyword search over a rendered list | `active_search` / `active_search_input` |
//! | Select a single related record and store its ID | `autocomplete_input` |
//! | Select many values or tags, as removable chips (issue #3410) | `autocomplete_input` with `.multiple()` |
//! | Pre-fill either mode from a `Changeset` | `autocomplete_input_for` |
//! | Plain `GET` form is sufficient | `axum::extract::Query` |
//! | You need unusual htmx wiring | Hand-write `hx-*` attributes |
//!
//! # Bulk actions
//!
//! | Widget | Use |
//! |--------|-----|
//! | `bulk_select_checkbox` | One row's `name="ids"` select checkbox (issue #1312) |
//! | `bulk_actions_toolbar` | `role="group"` submit button applying the selection |
//! | `bulk_actions_form` | `POST` form wrapping a list + toolbar, with the CSRF field |
//!
//! # Modals & confirmation
//!
//! | Situation | Use |
//! |-----------|-----|
//! | Custom dialog content | `modal` |
//! | Panel from an edge (filters, menus, details) | `drawer` |
//! | Confirm a destructive action (delete, etc.) without `window.confirm()` | `confirm_action` |
//!
//! # Integration with the repository full-text search feature
//!
//! The widgets wire up the client side; your handler owns the Diesel query.
//! If your repository has `#[repository(..., searchable)]`, the generated
//! `repo.search(q)` method works directly with these widgets:
//!
//! ```rust,ignore
//! use autumn_web::prelude::*;
//! use autumn_web::widgets::{ActiveSearchConfig, active_search};
//! use serde::Deserialize;
//!
//! #[derive(Deserialize)]
//! struct SearchQuery { q: String }
//!
//! #[get("/posts")]
//! async fn index() -> Markup {
//! let config = ActiveSearchConfig::new("/posts/search", "#post-results");
//! html! {
//! (active_search("post-search", "Search posts", &config))
//! }
//! }
//!
//! #[get("/posts/search")]
//! async fn search(
//! Query(params): Query<SearchQuery>,
//! repo: PgPostRepository,
//! ) -> AutumnResult<Markup> {
//! if params.q.trim().is_empty() {
//! return Ok(active_search_empty_state("Enter a search term"));
//! }
//! let results = repo.search(¶ms.q).await?;
//! Ok(html! {
//! @if results.is_empty() {
//! (active_search_empty_state("No results found"))
//! } @else {
//! @for post in &results { li { (post.title) } }
//! }
//! })
//! }
//! ```
//!
//! # No-JavaScript fallback
//!
//! `active_search` and `autocomplete_input` include a `<noscript>` block
//! with a plain HTML form or select that works without JavaScript. Your handler
//! already returns an HTML fragment — the only addition for a full no-JS page
//! is wrapping the response in your layout template.
// The core modules compile with `maud`. `nav` also holds `localized_path`,
// which needs no `maud`. Each opt-in family has its own feature.
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;
// The toast constants need no `maud`, as before the split: an app without
// `maud` can still target the region in an OOB swap or an `HX-Trigger`.
/// The default id for the `toast_region` container, and the OOB target
/// `toast` appends into.
///
/// Drop `toast_region(DEFAULT_TOAST_REGION_ID)` once in your base layout;
/// `toast(msg, variant)` then appends into it out-of-band.
pub const DEFAULT_TOAST_REGION_ID: &str = "toast-region";
/// The time a toast shows before it fades out, when you do not set
/// `ToastConfig::duration`. The stylesheet default (`5s`) must match.
pub const DEFAULT_TOAST_DURATION: Duration = from_secs;
/// Strip a leading `#` from a CSS ID selector to get a bare element ID.
///
/// `aria-controls` takes an ID (no `#`), while `hx-target` takes a CSS selector.
/// Build a root element's `class` string, merging `base` with any
/// caller-supplied extra class. Shared by every widget with a `.class()`
/// escape hatch (`card`, `hero`, ...).
/// Render `content` inside the `<h1>`–`<h6>` element selected by `level`, with
/// `class` applied and an optional `id` (used by [`modal`] so `aria-labelledby`
/// has a target). Shared by every widget with a configurable heading level
/// (`card`, `hero`, `modal`, ...) so an `HeadingLevel` variant is matched in
/// one place.
// ── Deterministic hashing (badge/avatar colors) ────────────────────────────
/// FNV-1a 64-bit hash of `s`. Deterministic across processes and platforms
/// (unlike `std`'s `DefaultHasher`), so the same input always maps to the same
/// badge variant / avatar color on every render and every machine.
/// Panic in a debug build when `label` gives no accessible name.