git-ents.gitmain
⌘K
foforge
commit 7c4c1d9
roots: 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

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

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) + }, )) }