roots: title-case page and form headings and unify empty states
commit
92292a2roots: title-case page and form headings and unify empty states
Static route titles, form headings, and button labels get real Title-Case; tab labels, card headers, badges, and the meta rail keep their lowercase mono chrome. A shared pages::blankslate helper replaces the header-only effects table, the bare toolchains and inbox rules, and the plain no-issues text; the search page distinguishes an untyped query (a type-to-search prompt naming the header’s search input) from a query with no matches; and the files root drops its self-referencing crumb.
Assisted-by: Claude:claude-fable-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/cli/ents-web/tests/router.rs
@@ -1761,7 +1761,7 @@
.to_vec(),
)
.expect("utf8 html");
- assert!(first_body.contains("conversation"));
+ assert!(first_body.contains("Conversation"));
assert!(first_body.contains("left at the first commit"));
assert!(first_body.contains("commenter"));
assert!(
@@ -1837,7 +1837,7 @@
// The commit page renders the review's verdict, body, and reviewer.
let page = get_body(&router, &format!("/commit/{oid}")).await;
- assert!(page.contains("reviews"), "the reviews section renders");
+ assert!(page.contains("Reviews"), "the reviews section renders");
assert!(
page.contains("class=\"verdict\""),
"the verdict renders prominently"
@@ -1982,7 +1982,7 @@
let detail = get_body(&router, &format!("/issues/{id}")).await;
assert!(detail.contains("gate rejects a valid signature"));
assert!(detail.contains("the full body"));
- assert!(detail.contains("discussion"));
+ assert!(detail.contains("Discussion"));
// A comment naming the issue as its context joins the thread.
let (cookie, csrf) = session_cookie_and_csrf(&router, &state, "/issues").await;
@@ -2098,8 +2098,9 @@
assert!(body.contains("/files/src/needle.rs"));
}
-/// `GET /search` with no query renders a friendly blankslate rather than
-/// an empty or error page.
+/// `GET /search` with no query renders a "type to search" blankslate
+/// naming the header's own search input -- not a "no matches" one, since
+/// nothing was searched yet -- rather than an empty or error page.
#[tokio::test]
async fn search_with_no_query_renders_a_blankslate() {
let state = build_state(FixtureIdentity {
@@ -2124,7 +2125,11 @@
.expect("body")
.to_bytes();
let body = String::from_utf8(body.to_vec()).expect("utf8 html");
- assert!(body.contains("No matches"));
+ assert!(body.contains("Type to search"));
+ assert!(
+ body.contains("Jump to file or symbol"),
+ "the prompt names the header's own search input"
+ );
}
/// `GET /toolchains` lists a toolchain written by an older schema (piece
crates/cli/ents-web/src/pages/account.rs
@@ -55,16 +55,16 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::Account,
- "account",
+ "Account",
html! {
div.readable {
(view)
- h2 { "create or update" }
+ h2 { "Create or Update" }
form method="post" action="/account" {
(super::csrf_input(&session))
label { "member" input type="text" name="member" value=(member_value); }
label { "login" input type="text" name="login" value=(login_value); }
- button type="submit" { "save" }
+ button type="submit" { "Save" }
}
}
},
crates/cli/ents-web/src/pages/comments.rs
@@ -83,18 +83,25 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::Comments,
- "comments",
+ "Comments",
html! {
div.readable {
- ul {
- @for (id, comment) in &rows {
- li {
- a href=(format!("/comments/{id}")) { (ents_forge::abbreviate_id(id)) }
- ": " (comment.body)
+ @if rows.is_empty() {
+ (super::blankslate(
+ "No comments yet",
+ html! { "Anchor one to a file with the form below." },
+ ))
+ } @else {
+ ul {
+ @for (id, comment) in &rows {
+ li {
+ a href=(format!("/comments/{id}")) { (ents_forge::abbreviate_id(id)) }
+ ": " (comment.body)
+ }
}
}
}
- h2 { "add a comment" }
+ h2 { "Add a Comment" }
(add_form(&query.rev, &session, &query.file, &query.lines))
}
},
@@ -148,7 +155,7 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::Comments,
- ents_forge::abbreviate_id(&id),
+ &format!("Comment {}", ents_forge::abbreviate_id(&id)),
html! {
(super::child_crumbs("comments", "/comments", ents_forge::abbreviate_id(&id)))
div.readable {
@@ -326,19 +333,19 @@
(super::csrf_input(session))
input type="hidden" name="return_to" value=(return_to);
label { "reply" textarea name="body" {} }
- button type="submit" { "reply" }
+ button type="submit" { "Reply" }
}
@if resolved {
form method="post" action=(format!("/comments/{id}/reopen")) {
(super::csrf_input(session))
input type="hidden" name="return_to" value=(return_to);
- button type="submit" { "reopen" }
+ button type="submit" { "Reopen" }
}
} @else {
form method="post" action=(format!("/comments/{id}/resolve")) {
(super::csrf_input(session))
input type="hidden" name="return_to" value=(return_to);
- button type="submit" { "resolve" }
+ button type="submit" { "Resolve" }
}
}
}
@@ -423,7 +430,7 @@
label { "rev" input type="text" name="rev" value=(default_rev); }
label { "lines" input type="text" name="lines" value=(prefill_lines); }
label { "body" textarea name="body" {} }
- button type="submit" { "comment" }
+ button type="submit" { "Comment" }
}
}
}
crates/cli/ents-web/src/pages/commits.rs
@@ -94,7 +94,7 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::Commits,
- "commits",
+ "Commits",
html! {
@if rows.is_empty() {
(blankslate())
@@ -188,17 +188,13 @@
(rows, older)
}
-/// The empty-history placeholder: an unborn `HEAD`, or a repository this
-/// page could not open at all.
+/// The empty-history placeholder ([`super::blankslate`]): an unborn
+/// `HEAD`, or a repository this page could not open at all.
fn blankslate() -> Markup {
- html! {
- div.card {
- div.blankslate {
- h2 { "No commits yet" }
- p { "This repository has no history to show." }
- }
- }
- }
+ super::blankslate(
+ "No commits yet",
+ html! { "This repository has no history to show." },
+ )
}
/// `GET /commit/{oid}`: a single commit's full message, metadata, and a
@@ -303,7 +299,7 @@
}
@if !comments.is_empty() {
div.readable {
- h2 { "conversation" }
+ h2 { "Conversation" }
@for (index, comment) in comments.iter().enumerate() {
(super::comments::comment_card(index, comment, super::comments::LinkMode::CrossFile))
}
@@ -339,7 +335,7 @@
.unwrap_or_default();
let return_to = format!("/commit/{oid}");
html! {
- h2 { "reviews" }
+ h2 { "Reviews" }
@for ((target, member), review) in &reviews {
div.card {
div.comment-meta {
@@ -384,7 +380,7 @@
(super::csrf_input(session))
input type="hidden" name="return_to" value=(return_to);
label { "comment on this review" textarea name="body" {} }
- button type="submit" { "comment" }
+ button type="submit" { "Comment" }
}
}
}
@@ -468,7 +464,7 @@
option value="request-changes" {}
}
label { "body" textarea name="body" {} }
- button type="submit" { "start a review" }
+ button type="submit" { "Start a Review" }
}
}
}
crates/cli/ents-web/src/pages/dashboard.rs
@@ -52,7 +52,7 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::Overview,
- "overview",
+ "Overview",
html! {
div.overview {
div { (main) }
@@ -171,17 +171,13 @@
}
}
-/// The empty-column placeholder shown when the repository has no `README`,
-/// no readable root, or no `HEAD` at all.
+/// The empty-column placeholder ([`super::blankslate`]) shown when the
+/// repository has no `README`, no readable root, or no `HEAD` at all.
fn blankslate() -> Markup {
- html! {
- div.card {
- div.blankslate {
- h2 { "Nothing to show yet" }
- p { "Add a " code { "README" } " or browse the repository in " a href="/files" { "Files" } "." }
- }
- }
- }
+ super::blankslate(
+ "Nothing to show yet",
+ html! { "Add a " code { "README" } " or browse the repository in " a href="/files" { "Files" } "." },
+ )
}
/// The first root-tree blob whose stem is `README` and whose extension
crates/cli/ents-web/src/pages/effects.rs
@@ -27,12 +27,20 @@
O: Find + Write + Send + 'static,
{
let rows = read_all(&state)?;
+ let body = if rows.is_empty() {
+ super::blankslate(
+ "No effects yet",
+ html! { "Registered effects and their trigger queries appear here." },
+ )
+ } else {
+ crate::render::list_table(&rows, "name", |id| format!("/effects/{id}"))
+ };
Ok(super::layout_meta(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
"/effects",
- "effects",
- crate::render::list_table(&rows, "name", |id| format!("/effects/{id}")),
+ "Effects",
+ body,
))
}
crates/cli/ents-web/src/pages/files.rs
@@ -123,9 +123,8 @@
&super::RepoHeader::from_state(state),
&super::identity_label(state),
super::Tab::Files,
- "files",
+ "Files",
html! {
- (crumbs(path))
(dir_listing(path, Vec::new()))
},
));
@@ -143,9 +142,8 @@
&super::RepoHeader::from_state(state),
&super::identity_label(state),
super::Tab::Files,
- "files",
+ "Files",
html! {
- (crumbs(path))
(dir_listing(path, entries))
},
));
@@ -283,7 +281,10 @@
/// `path`, `chevron-right` icons separating segments -- pure navigation,
/// no trailing actions. The history/comment links that used to trail this
/// nav on a blob view now live in [`blob_header`]'s own action group
-/// instead (see this module's own top-level doc for why).
+/// instead (see this module's own top-level doc for why). The files root
+/// itself renders no crumbs at all: a lone self-referencing "files" crumb
+/// under the page's own "Files" title (and above the listing card's own
+/// "files" header) named the same place three times.
fn crumbs(path: &str) -> Markup {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut acc = String::new();
@@ -652,7 +653,7 @@
return html! {};
}
html! {
- h2 { "outdated comments" }
+ h2 { "Outdated Comments" }
@for &(index, comment) in comments {
(super::comments::comment_card(index, comment, super::comments::LinkMode::SameFile))
}
crates/cli/ents-web/src/pages/inbox.rs
@@ -29,11 +29,19 @@
rows.push(rest.to_owned());
}
}
+ let body = if rows.is_empty() {
+ super::blankslate(
+ "Inbox is empty",
+ maud::html! { "Entries awaiting adoption appear here." },
+ )
+ } else {
+ crate::render::string_list(&rows, |_| "/inbox".to_owned())
+ };
Ok(super::layout_meta(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
"/inbox",
- "inbox",
- crate::render::string_list(&rows, |_| "/inbox".to_owned()),
+ "Inbox",
+ body,
))
}
crates/cli/ents-web/src/pages/issues.rs
@@ -49,11 +49,14 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::Issues,
- "issues",
+ "Issues",
html! {
div.readable {
@if rows.is_empty() {
- p { "No issues yet." }
+ (super::blankslate(
+ "No issues yet",
+ html! { "Open one with the form below." },
+ ))
} @else {
table.entity-list {
thead {
@@ -71,7 +74,7 @@
}
}
}
- h2 { "open an issue" }
+ h2 { "Open an Issue" }
(new_form(&session))
}
},
@@ -121,12 +124,12 @@
div.doc-body { (body) }
}
details {
- summary { "edit" }
+ summary { "Edit" }
(edit_form(&session, &issue))
}
- h2 { "discussion" }
+ h2 { "Discussion" }
(crate::pages::comments::thread_section(&state, &session, &thread, &return_to))
- h2 { "add a comment" }
+ h2 { "Add a Comment" }
(comment_form(&session, &id))
}
},
@@ -326,7 +329,7 @@
label { "assignees" input type="text" name="assignees" placeholder="alice, bob"; }
label { "labels" input type="text" name="labels" placeholder="bug, gate"; }
label { "body" textarea name="body" {} }
- button type="submit" { "open issue" }
+ button type="submit" { "Open Issue" }
}
}
}
@@ -348,7 +351,7 @@
input type="text" name="assignees" value=(join_members(&issue.assignees));
}
label { "labels" input type="text" name="labels" value=(issue.labels.join(", ")); }
- button type="submit" { "save" }
+ button type="submit" { "Save" }
}
}
}
@@ -373,7 +376,7 @@
form method="post" action=(format!("/issues/{id}/comment")) {
(super::csrf_input(session))
label { "body" textarea name="body" {} }
- button type="submit" { "comment" }
+ button type="submit" { "Comment" }
}
}
}
crates/cli/ents-web/src/pages/members.rs
@@ -23,12 +23,20 @@
O: Find + Write + Send + 'static,
{
let rows = read_all(&state)?;
+ let body = if rows.is_empty() {
+ super::blankslate(
+ "No members yet",
+ maud::html! { "Enroll one with " code { "git ents members add" } "." },
+ )
+ } else {
+ crate::render::list_table(&rows, "username", |id| format!("/members/{id}"))
+ };
Ok(super::layout_meta(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
"/members",
- "members",
- crate::render::list_table(&rows, "username", |id| format!("/members/{id}")),
+ "Members",
+ body,
))
}
crates/cli/ents-web/src/pages/meta.rs
@@ -21,7 +21,7 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::Meta,
- "meta",
+ "Meta",
html! {
div.card {
div.card-header { "meta" }
crates/cli/ents-web/src/pages/mod.rs
@@ -390,6 +390,23 @@
}
}
+/// The shared empty-state card (`ents.css`'s `.blankslate`): a short
+/// title and one explanatory line, rendered instead of a bare list or a
+/// header-only table when a page family has nothing to show yet. `line`
+/// is markup, not text, so a page can point at its own create form or
+/// link a next step ([`super::dashboard`]'s README pointer does the
+/// same).
+pub(crate) fn blankslate(title: &str, line: Markup) -> Markup {
+ html! {
+ div.card {
+ div.blankslate {
+ h2 { (title) }
+ p { (line) }
+ }
+ }
+ }
+}
+
/// The one-level breadcrumb trail every `/{id}` child page renders above
/// its own content -- "parent \u{203a} here", reusing the `.crumbs` markup
/// pattern [`super::files`]'s own multi-level path trail already renders
crates/cli/ents-web/src/pages/redactions.rs
@@ -22,12 +22,20 @@
O: Find + Write + Send + 'static,
{
let rows = read_all(&state)?;
+ let body = if rows.is_empty() {
+ super::blankslate(
+ "No redactions yet",
+ maud::html! { "Record one with " code { "git ents redact add" } "." },
+ )
+ } else {
+ crate::render::list_table(&rows, "id", |id| format!("/redactions/{id}"))
+ };
Ok(super::layout_meta(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
"/redactions",
- "redactions",
- crate::render::list_table(&rows, "id", |id| format!("/redactions/{id}")),
+ "Redactions",
+ body,
))
}
crates/cli/ents-web/src/pages/search.rs
@@ -28,8 +28,9 @@
#[derive(Debug, Deserialize)]
pub struct SearchQuery {
/// The search term. Empty (the default, and what a bare `GET /search`
- /// carries) renders the same friendly blankslate as a query with no
- /// matches.
+ /// carries) renders a "type to search" blankslate rather than a
+ /// no-matches one -- nothing was searched yet, so nothing "failed to
+ /// match" (see [`blankslate`]'s own doc).
#[serde(default)]
q: String,
}
@@ -63,7 +64,7 @@
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
super::Tab::None,
- "search",
+ "Search",
html! {
@if !any_matches {
(blankslate(&query))
@@ -223,20 +224,25 @@
}
}
-/// The empty-results placeholder, shown for an empty query and for a
-/// non-empty one with no matches at all.
+/// The empty-results placeholder ([`super::blankslate`]): a "type to
+/// search" prompt before any query has been typed at all (naming the
+/// header's own "Jump to file or symbol" search input, this page's only
+/// entry point), or a "no matches" note for a non-empty query that found
+/// nothing.
fn blankslate(query: &str) -> Markup {
- html! {
- div.card {
- div.blankslate {
- h2 { "No matches" }
- @if query.is_empty() {
- p { "Type a search term above to look through files and meta entities." }
- } @else {
- p { "Nothing matched " code { (query) } "." }
- }
- }
- }
+ if query.is_empty() {
+ super::blankslate(
+ "Type to search",
+ html! {
+ "Use the header's \u{201c}Jump to file or symbol\u{201d} search "
+ "to look through files and meta entities."
+ },
+ )
+ } else {
+ super::blankslate(
+ "No matches",
+ html! { "Nothing matched " code { (query) } "." },
+ )
}
}
crates/cli/ents-web/src/pages/toolchains.rs
@@ -47,11 +47,12 @@
(name, detail)
})
.collect();
- Ok(super::layout_meta(
- &super::RepoHeader::from_state(&state),
- &super::identity_label(&state),
- "/toolchains",
- "toolchains",
+ let body = if rows.is_empty() {
+ super::blankslate(
+ "No toolchains yet",
+ html! { "Import one with " code { "git ents toolchain import" } "." },
+ )
+ } else {
html! {
div.card {
ul.string-list {
@@ -65,7 +66,14 @@
}
}
}
- },
+ }
+ };
+ Ok(super::layout_meta(
+ &super::RepoHeader::from_state(&state),
+ &super::identity_label(&state),
+ "/toolchains",
+ "Toolchains",
+ body,
))
}
@@ -106,7 +114,7 @@
dt { "name" } dd { (toolchain.name) }
dt { "recipe" } dd { (format!("{recipe:?}")) }
}
- h2 { "import log" }
+ h2 { "Import Log" }
ul {
@for oid in &log {
li { (oid.to_string()) }