roots: expand the tab strip to six truthful tabs with child-page crumbs
commit
7c4c1d9roots: expand the tab strip to six truthful tabs with child-page crumbs
Commits and issues become tabs of their own (Tab::Commits, Tab::Issues); issues leaves the meta rail, which keeps members, effects, toolchains, redactions, and inbox. Every /{id} child page now renders a one-level breadcrumb trail via the shared child_crumbs helper, and the tab strip hides its stray macOS scrollbar while staying scrollable on narrow viewports.
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
@@ -966,6 +966,43 @@
assert!(body.contains("class=\"keyword\""));
}
+/// The tab strip (`crate::pages::layout`) names every top-level page
+/// family truthfully: six tabs -- overview, files, commits, issues,
+/// comments, meta -- and the issues family no longer renders behind the
+/// `META_SECTIONS` rail (it is a tab of its own; see `crate::pages::mod`'s
+/// own doc).
+#[tokio::test]
+async fn the_tab_strip_carries_all_six_tabs_and_issues_left_the_meta_rail() {
+ let state = build_state(FixtureIdentity {
+ name: "local-user",
+ key: Keypair::from_seed(1),
+ });
+ let router = ents_web::router(state);
+
+ let overview = get_body(&router, "/").await;
+ for href in ["/", "/files", "/commits", "/issues", "/comments", "/meta"] {
+ assert!(
+ overview.contains(&format!("href=\"{href}\"")),
+ "the tab strip links {href}"
+ );
+ }
+
+ let issues = get_body(&router, "/issues").await;
+ assert!(
+ !issues.contains("class=\"meta-rail\""),
+ "issues renders as its own tab, not behind the meta rail"
+ );
+
+ // The rail renders a bare (classless when inactive) link per section;
+ // the tab strip's own issues link always carries `class="tab..."`, so
+ // this exact form only ever comes from the rail.
+ let members = get_body(&router, "/members").await;
+ assert!(
+ !members.contains("<a href=\"/issues\">issues</a>"),
+ "the meta rail no longer lists issues"
+ );
+}
+
/// The `meta` tab restructure (`crate::pages::mod`'s own doc): `GET /meta`
/// is reachable as the tab's index page, and `GET /members` -- one of the
/// five page families that group now shares -- renders with the
crates/cli/ents-web/src/assets/ents.css
@@ -13,7 +13,7 @@
* stacks, so this sheet never depends on a network fetch. Added beyond the
* pre-redo sheet: `.meta-layout`/`.meta-rail` for the `meta` tab's
* page-family rail (`crate::pages::META_SECTIONS`) and `.id-chip` for the
- * header's signing-identity link, both new to this crate's own four-tab
+ * header's signing-identity link, both new to this crate's own tab
* restructure; `.comment-meta`/`.outdated` for the file-anchored
* comment cards `crate::pages::comments::comments_section` renders below a
* blob view, which otherwise reuse `.card`/`.doc-body` as-is; and
@@ -122,7 +122,12 @@
.branch { font-family: var(--font-mono); font-size: .72rem; font-weight: 600; color: var(--color-accent); background: var(--color-accent-subtle); border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent); border-radius: var(--radius-pill); padding: .1rem .6rem; display: inline-flex; align-items: center; gap: .3rem; }
.branch .icon { width: 13px; height: 13px; color: var(--color-accent); }
-.tabs { display: flex; gap: .15rem; max-width: var(--max-width); margin: 0 auto; padding: 0 1.5rem; border-bottom: 1px solid var(--color-border); overflow-x: auto; }
+/* `overflow-x: auto` keeps the strip scrollable on a narrow viewport;
+ * `scrollbar-width: none` (and the WebKit pseudo-element below) hides the
+ * scrollbar itself, which otherwise renders as a stray artifact under the
+ * strip on macOS. */
+.tabs { display: flex; gap: .15rem; max-width: var(--max-width); margin: 0 auto; padding: 0 1.5rem; border-bottom: 1px solid var(--color-border); overflow-x: auto; scrollbar-width: none; }
+.tabs::-webkit-scrollbar { display: none; }
.tab { display: inline-flex; align-items: center; gap: .4rem; padding: 10px 14px; font-size: .88rem; color: var(--color-text-muted); text-decoration: none; white-space: nowrap; position: relative; transition: color .15s; }
.tab:hover { color: var(--color-text); text-decoration: none; }
.tab.active { color: var(--color-text); font-weight: 600; }
crates/cli/ents-web/src/pages/comments.rs
@@ -148,6 +148,7 @@
super::Tab::Comments,
ents_forge::abbreviate_id(&id),
html! {
+ (super::child_crumbs("comments", "/comments", ents_forge::abbreviate_id(&id)))
dl {
dt { "state" } dd { (comment.state) }
@if let Some(context) = &comment.context {
crates/cli/ents-web/src/pages/commits.rs
@@ -1,7 +1,7 @@
//! `GET /commits`, `GET /commit/{oid}`: a read-only commit history and
-//! per-commit unified diff over `HEAD` -- a view of the code, not a tab of
-//! its own (both routes render with `super::Tab::Files` active; see
-//! [`super`]'s own doc), reached from [`super::files`]'s "history" link.
+//! per-commit unified diff over `HEAD` -- a tab of its own (both routes
+//! render with `super::Tab::Commits` active; see [`super`]'s own doc),
+//! also reached from [`super::files`]'s "history" link.
//!
//! Reads go through `gix`'s high-level `Repository`/`Commit`/`Tree` types,
//! opened fresh per request from `state.path`, exactly as
@@ -93,7 +93,7 @@
Ok(super::layout(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
- super::Tab::Files,
+ super::Tab::Commits,
"commits",
html! {
@if rows.is_empty() {
@@ -259,9 +259,10 @@
Ok(super::layout(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
- super::Tab::Files,
+ super::Tab::Commits,
&subject,
html! {
+ (super::child_crumbs("commits", "/commits", &super::short_oid(&object_id)))
div.card {
div.card-header { "commit " code { (super::short_oid(&object_id)) } }
div.commit {
crates/cli/ents-web/src/pages/effects.rs
@@ -77,7 +77,10 @@
&super::identity_label(&state),
"/effects",
&name,
- body,
+ html! {
+ (super::child_crumbs("effects", "/effects", &name))
+ (body)
+ },
))
}
crates/cli/ents-web/src/pages/issues.rs
@@ -1,8 +1,9 @@
//! `GET /issues`, `GET /issues/{id}`, `POST /issues`,
//! `POST /issues/{id}`, `POST /issues/{id}/comment`: the issue surface
-//! (`model.issue`), an entity family under the `meta` tab's registry
-//! (`crate::pages::META_SECTIONS`) exactly as members, effects, and the
-//! rest are.
+//! (`model.issue`), a top-level tab of its own (`crate::pages::Tab::Issues`;
+//! see [`super`]'s own doc) rather than an entry in the `meta` tab's
+//! registry -- issues are a working surface like comments, not repository
+//! metadata.
//!
//! Every read is `ents_forge::issue::{list,show}` and every mutation is
//! `ents_forge::issue::{new,edit}` or `ents_forge::comment::add` -- the web
@@ -44,10 +45,10 @@
O: Find + Write + Send + 'static,
{
let rows = issue::list(state.refs.as_ref(), &*state.objects())?;
- Ok(super::layout_meta(
+ Ok(super::layout(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
- "/issues",
+ super::Tab::Issues,
"issues",
html! {
@if rows.is_empty() {
@@ -101,12 +102,13 @@
let body =
crate::asciidoc::to_html(&issue.body).unwrap_or_else(|_| html! { p { (issue.body) } });
let return_to = format!("/issues/{id}");
- Ok(super::layout_meta(
+ Ok(super::layout(
&super::RepoHeader::from_state(&state),
&super::identity_label(&state),
- "/issues",
+ super::Tab::Issues,
&issue.title,
html! {
+ (super::child_crumbs("issues", "/issues", ents_forge::abbreviate_id(&id)))
div.card {
dl {
dt { "state" } dd { span.comment-state { (issue.state) } }
crates/cli/ents-web/src/pages/members.rs
@@ -62,7 +62,10 @@
&super::identity_label(&state),
"/members",
&username,
- body,
+ maud::html! {
+ (super::child_crumbs("members", "/members", &username))
+ (body)
+ },
))
}
crates/cli/ents-web/src/pages/mod.rs
@@ -9,15 +9,15 @@
//! [`toolchains`], [`comments`], and [`issues`] are legitimate custom pages
//! (`ents-kiln`'s recipe provenance and `ents-forge`'s anchor projection
//! and issue threads all need domain-specific rendering no generic
-//! reflection walk should grow special cases for). [`issues`], [`members`],
+//! reflection walk should grow special cases for). [`members`],
//! [`effects`], [`toolchains`], [`redactions`], and [`inbox`] additionally
//! share one `meta` tab and `META_SECTIONS` rail rather than each carrying
//! its own top-level tab (see `Tab`'s own doc); [`meta`] is that group's
-//! `GET /meta` landing page. [`commits`] is a view of the code, not a tab of its own -- both
-//! its routes render with `Tab::Files` active, reached from
-//! [`files`]'s own "history" link. [`search`] renders with no tab active
-//! at all, like [`account`]; it is reached from `layout`'s own nav
-//! search form rather than any tab.
+//! `GET /meta` landing page. [`commits`] and [`issues`] are tabs of their
+//! own -- `Tab::Commits` and `Tab::Issues` in [`layout`]'s six-tab strip,
+//! alongside overview, files, comments, and meta. [`search`] renders with
+//! no tab active at all, like [`account`]; it is reached from `layout`'s
+//! own nav search form rather than any tab.
pub mod account;
pub mod comments;
@@ -105,10 +105,10 @@
/// [`layout`]'s nav bar, so a handler can name which tab it renders behind
/// without `layout` re-deriving it from the request path (mirrors
/// `pre-redo:crates/git-ents-server/src/web/pages.rs`'s own `Tab` enum,
-/// trimmed to four primary tabs). `Meta` covers six page families
-/// ([`super::issues`], [`super::members`], [`super::effects`],
-/// [`super::toolchains`], [`super::redactions`], [`super::inbox`]) behind
-/// one tab and the
+/// at this crate's own six primary tabs: overview, files, commits, issues,
+/// comments, meta). `Meta` covers five page families ([`super::members`],
+/// [`super::effects`], [`super::toolchains`], [`super::redactions`],
+/// [`super::inbox`]) behind one tab and the
/// [`META_SECTIONS`] rail (see [`layout_meta`]) rather than a tab each --
/// nine equal tabs did not scale as page families grew. `Account` matches
/// none of [`layout`]'s tab-strip arms, so it highlights nothing there;
@@ -120,6 +120,8 @@
pub(crate) enum Tab {
Overview,
Files,
+ Commits,
+ Issues,
Comments,
Meta,
Account,
@@ -147,11 +149,6 @@
/// The `meta` tab's registry (see [`MetaSection`]'s own doc).
pub(crate) const META_SECTIONS: &[MetaSection] = &[
- MetaSection {
- name: "issues",
- href: "/issues",
- blurb: "Filed issues and their discussion threads.",
- },
MetaSection {
name: "members",
href: "/members",
@@ -271,6 +268,8 @@
nav.tabs {
a.tab.active[active == Tab::Overview] href="/" { "overview" }
a.tab.active[active == Tab::Files] href="/files" { "files" }
+ a.tab.active[active == Tab::Commits] href="/commits" { "commits" }
+ a.tab.active[active == Tab::Issues] href="/issues" { "issues" }
a.tab.active[active == Tab::Comments] href="/comments" { "comments" }
a.tab.active[active == Tab::Meta] href="/meta" { "meta" }
}
@@ -391,6 +390,23 @@
}
}
+/// 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
+/// (same `nav.crumbs`/`span.sep`/`span.here` classes, so the stylesheet
+/// needs no second breadcrumb rule). `parent` links back to the family's
+/// list page at `parent_href`; `here` is the child's own display name, a
+/// plain non-link "you are here" crumb.
+pub(crate) fn child_crumbs(parent: &str, parent_href: &str, here: &str) -> Markup {
+ html! {
+ nav.crumbs {
+ a href=(parent_href) { (parent) }
+ span.sep { (crate::assets::icon_chevron()) }
+ span.here { (here) }
+ }
+ }
+}
+
/// A commit id shortened to seven hex characters for display -- mirrors
/// `pre-redo:crates/git-ents-server/src/web/pages.rs`'s own `short_oid`.
/// Falls back to the full id on the (practically unreachable) case that a
crates/cli/ents-web/src/pages/redactions.rs
@@ -61,7 +61,10 @@
&super::identity_label(&state),
"/redactions",
&id,
- body,
+ maud::html! {
+ (super::child_crumbs("redactions", "/redactions", &id))
+ (body)
+ },
))
}
crates/cli/ents-web/src/pages/toolchains.rs
@@ -126,6 +126,9 @@
&super::identity_label(&state),
"/toolchains",
&name,
- body,
+ html! {
+ (super::child_crumbs("toolchains", "/toolchains", &name))
+ (body)
+ },
))
}