Skip to main content

max / makenotwork

Describe the git nav bar and the notes panel The gate on the source browser: ten git templates include the nav and two include the notes panel, so neither page behind them could become a described screen while the shared markup was hand-written. The nav is a select and a list. The tab strip is Node::List with Row::current, which is the member for a set of addresses with one of them current, and is what is-selected said. The ref chooser is a select field whose answer goes somewhere, so it writes a navigating read; the canonical tree address is a path segment and a described write submits a parameter, so GET /git/{owner}/{repo}/ref joins the two and redirects. One address for the content, one for the control. issues, issue and settings each carried their own four-link copy of the strip. They take the shared one now and gain Tags and Notes with it. They pass no refs and draw no chooser: those pages are about the repository rather than about a ref, and their handlers never open it. The notes panel keeps its rendered markdown in a Handover, since docengine and the sanitiser have already run and neither is derivable downstream. The namespace is an inline Node::Code, which is what the shipped rule set font-mono on it for. A namespace carries slashes, so it is slugged before it becomes an element id.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-02 16:57 UTC
Signed with PGP, not checked
Commit: 8f3e3eecdf0e324a82863a4a0f70904266668df6
Parent: ab04f77
12 files changed, +758 insertions, -115 deletions
M server/Cargo.lock +12 -12
@@ -10759,6 +10759,18 @@
10759 10759 "pkg-config",
10760 10760 ]
10761 10761
10762 + [[patch.unused]]
10763 + name = "kberg"
10764 + version = "0.1.0"
10765 +
10766 + [[patch.unused]]
10767 + name = "ops-status"
10768 + version = "0.1.0"
10769 +
10770 + [[patch.unused]]
10771 + name = "painhours"
10772 + version = "0.1.0"
10773 +
10762 10774 [[patch.unused]]
10763 10775 name = "synckit-client"
10764 10776 version = "0.10.0"
@@ -10782,15 +10794,3 @@
10782 10794 [[patch.unused]]
10783 10795 name = "quasi-tauri"
10784 10796 version = "0.99.0"
10785 -
10786 - [[patch.unused]]
10787 - name = "kberg"
10788 - version = "0.1.0"
10789 -
10790 - [[patch.unused]]
10791 - name = "ops-status"
10792 - version = "0.1.0"
10793 -
10794 - [[patch.unused]]
10795 - name = "painhours"
10796 - version = "0.1.0"
@@ -6398,26 +6398,44 @@
6398 6398 .git-linked-project { margin-top: var(--gap-peer); font-size: var(--text-note); }
6399 6399 .git-linked-project a { color: var(--content); }
6400 6400
6401 - /* Ref bar (branch/tag selector + nav links) */
6402 - .git-ref-bar {
6401 + /* Ref bar (branch/tag chooser + the places the repo has).
6402 +
6403 + Described: `crate::quasi::widgets::git_nav` emits the region, the field and
6404 + the list, so these rules dress the renderer's own class names rather than
6405 + hand-written ones. The strip is a list drawn across, which is where "how a
6406 + set is laid out" has always lived. */
6407 + #git-nav { margin-bottom: var(--gap-section); font-size: var(--text-note); }
6408 + /* The run already lays out across and wraps; only the rhythm is the app's. */
6409 + #git-nav .run { gap: var(--gap-section); }
6410 + /* The label reads beside the control rather than over it: the bar is one line
6411 + and a stacked label would set its height. */
6412 + #git-nav .form-group {
6403 6413 display: flex;
6404 6414 align-items: center;
6405 - gap: var(--gap-section);
6406 - margin-bottom: var(--gap-section);
6407 - font-size: var(--text-note);
6415 + gap: var(--gap-peer);
6416 + margin: 0;
6408 6417 }
6409 - .git-ref-select {
6418 + #git-nav .form-label { margin: 0; white-space: nowrap; }
6419 + #git-nav select.field {
6420 + width: auto;
6410 6421 font-family: var(--font-mono);
6411 6422 font-size: var(--text-fine);
6412 6423 padding: var(--gap-bound) var(--gap-peer);
6413 - background: var(--surface-overlay);
6414 6424 border: 1px solid var(--border);
6415 6425 border-radius: var(--radius-control);
6416 6426 }
6417 - .git-nav-links { display: flex; gap: var(--gap-section); }
6418 - .git-nav-links a { text-decoration: none; color: var(--content); opacity: 0.6; }
6419 - .git-nav-links a:hover,
6420 - .git-nav-links a.is-selected { opacity: 1; }
6427 + /* The strip is the list drawn across. Everything else about a list -- the
6428 + markers, the box -- is the design system's and is not restated here. */
6429 + #git-nav .list { display: flex; gap: var(--gap-section); }
6430 + #git-nav .row {
6431 + margin: 0;
6432 + padding: 0;
6433 + border: 0;
6434 + background: none;
6435 + }
6436 + #git-nav .row-activate { text-decoration: none; color: var(--content); opacity: 0.6; }
6437 + #git-nav .row-activate:hover,
6438 + #git-nav .row-current .row-activate { opacity: 1; }
6421 6439
6422 6440 /* Breadcrumb navigation */
6423 6441 .git-breadcrumb {
@@ -6700,28 +6718,28 @@
6700 6718 .git-replace-table td { padding: var(--gap-peer) var(--gap-section) var(--gap-peer) 0; border-bottom: 1px solid var(--border); }
6701 6719 .git-replace-table a { color: var(--content); text-decoration: none; }
6702 6720 .git-replace-table a:hover { text-decoration: underline; }
6703 - /* Git notes (refs/notes/*) shown against the object they annotate */
6704 - .git-notes { margin-bottom: var(--gap-pane); }
6705 - .git-note {
6721 + /* Git notes (refs/notes/*) shown against the object they annotate.
6722 +
6723 + Described: `crate::quasi::widgets::git_notes` emits the panel, so these rules
6724 + dress the renderer's classes. The body is the one part that is still the
6725 + app's own markup -- it comes out of docengine already -- and it keeps
6726 + `.git-note-body`, which the annotations panel writes too. */
6727 + #notes { margin-bottom: var(--gap-pane); }
6728 + #notes > .region {
6706 6729 border: 1px solid var(--border);
6707 6730 border-left: 2px solid var(--content);
6708 6731 margin-bottom: var(--gap-section);
6709 6732 }
6710 - .git-note-header {
6711 - display: flex;
6712 - align-items: baseline;
6713 - justify-content: space-between;
6714 - gap: var(--gap-group);
6733 + /* The header is the note's leading row: the namespace on the left and where it
6734 + came from on the right. The auto margin is what puts it there; the shipped
6735 + rule used `space-between` on a header that held its own elements. */
6736 + #notes > .region > .region .run {
6715 6737 padding: var(--gap-peer) var(--gap-section);
6716 6738 border-bottom: 1px solid var(--border);
6717 6739 font-size: var(--text-fine);
6718 6740 }
6719 - .git-note-namespace { font-family: var(--font-mono); }
6720 - /* Sits beside the namespace rather than being spread across the header: the
6721 - auto margin absorbs the free space `space-between` would otherwise put
6722 - between the two, leaving the attribution on the right where it was. */
6723 - .git-note-owned { margin-right: auto; opacity: 0.6; }
6724 - .git-note-attribution { opacity: 0.6; }
6741 + #notes > .region > .region .run > :first-child { margin-right: auto; }
6742 + #notes .text { margin: 0; opacity: 0.6; }
6725 6743 .git-note-namespaces {
6726 6744 display: flex;
6727 6745 flex-wrap: wrap;
@@ -6983,7 +7001,7 @@
6983 7001
6984 7002 @media (max-width: 839px) {
6985 7003 /* Git browser mobile */
6986 - .git-ref-bar { flex-wrap: wrap; gap: var(--gap-peer); }
7004 + #git-nav .run { gap: var(--gap-peer); }
6987 7005 .git-repo-name { font-size: var(--text-subhead); }
6988 7006 .git-clone-url { font-size: var(--text-fine); word-break: break-all; }
6989 7007 .git-tree .icon { display: none; }
@@ -1,20 +1,13 @@
1 - <div class="git-ref-bar">
2 - <select class="git-ref-select" data-change="nav-to-value">
3 - {% for r in refs %}
4 - <option value="/git/{{ owner }}/{{ repo_name }}/tree/{{ r.name }}"
5 - {% if r.name == current_ref %}selected{% endif %}>
6 - {% if r.is_branch %}{% else %}tag: {% endif %}{{ r.name }}
7 - </option>
8 - {% endfor %}
9 - </select>
10 - <nav class="git-nav-links">
11 - <a href="/git/{{ owner }}/{{ repo_name }}/tree/{{ current_ref }}"{% if active_tab == "files" %} class="is-selected"{% endif %}>Files</a>
12 - <a href="/git/{{ owner }}/{{ repo_name }}/commits/{{ current_ref }}"{% if active_tab == "commits" || active_tab == "commit" %} class="is-selected"{% endif %}>Commits</a>
13 - <a href="/git/{{ owner }}/{{ repo_name }}/tags"{% if active_tab == "tags" %} class="is-selected"{% endif %}>Tags</a>
14 - <a href="/git/{{ owner }}/{{ repo_name }}/notes"{% if active_tab == "notes" %} class="is-selected"{% endif %}>Notes</a>
15 - <a href="/git/{{ owner }}/{{ repo_name }}/issues"{% if active_tab == "issues" %} class="is-selected"{% endif %}>Issues{% if open_issue_count > 0 %} ({{ open_issue_count }}){% endif %}</a>
16 - {% if is_owner %}
17 - <a href="/git/{{ owner }}/{{ repo_name }}/settings"{% if active_tab == "settings" %} class="is-selected"{% endif %}>Settings</a>
18 - {% endif %}
19 - </nav>
20 - </div>
1 + {# The repository nav bar. Described, not written: the members and the
2 + reasoning are in `crate::quasi::widgets::git_nav`, which the git pages will
3 + embed directly as they convert. Expects `refs`, `owner`, `repo_name`,
4 + `current_ref`, `active_tab`, `open_issue_count` and `is_owner`. #}
5 + {{ crate::quasi::widgets::git_nav::html(&crate::quasi::widgets::git_nav::Nav {
6 + owner: owner,
7 + repo: repo_name,
8 + current_ref: current_ref,
9 + active_tab: active_tab,
10 + open_issue_count: *open_issue_count,
11 + is_owner: *is_owner,
12 + refs: refs,
13 + })|safe }}
@@ -1,29 +1,5 @@
1 - {# Notes on the object a page is showing. Expects `notes: Vec<CommitNote>`.
2 - Shared by the commit page (notes on the commit) and the file view (notes on
3 - the blob), so the two cannot drift. #}
4 - {% if !notes.is_empty() %}
5 - <div class="git-notes" id="notes">
6 - {% for note in notes %}
7 - <div class="git-note">
8 - <div class="git-note-header">
9 - <span class="git-note-namespace">{{ note.namespace }}</span>
10 - {% if note.read_only %}
11 - {# Says where it came from, so a note nobody on the repo wrote does
12 - not read as one of theirs they have forgotten. #}
13 - <span class="git-note-owned">written by makenot.work</span>
14 - {% endif %}
15 - {% if let Some(a) = note.attribution %}
16 - <span class="git-note-attribution">
17 - {% if a.exact %}
18 - {{ a.by }} &middot; {{ a.when }}
19 - {% else %}
20 - edited since {{ a.short_commit }} &middot; {{ a.when }}
21 - {% endif %}
22 - </span>
23 - {% endif %}
24 - </div>
25 - <div class="git-note-body git-readme-body">{{ note.html|safe }}</div>
26 - </div>
27 - {% endfor %}
28 - </div>
29 - {% endif %}
1 + {# Notes on the object a page is showing. Described, not written: the members
2 + and the reasoning are in `crate::quasi::widgets::git_notes`. Shared by the
3 + commit page (notes on the commit) and the file view (notes on the blob), so
4 + the two cannot drift. Expects `notes: Vec<CommitNote>`. #}
5 + {{ crate::quasi::widgets::git_notes::html(notes)|safe }}
@@ -60,3 +60,5 @@
60 60
61 61 pub mod carousel;
62 62 pub mod code;
63 + pub mod git_nav;
64 + pub mod git_notes;
@@ -1023,3 +1023,59 @@
1023 1023 active_tab: "files",
1024 1024 })
1025 1025 }
1026 +
1027 + /// What the ref chooser submits. See [`crate::quasi::widgets::git_nav`].
1028 + #[derive(Deserialize)]
1029 + pub(super) struct RefChoice {
1030 + #[serde(rename = "ref")]
1031 + pub chosen: String,
1032 + }
1033 +
1034 + /// `GET /git/{owner}/{repo}/ref?ref=`: the ref chooser's destination.
1035 + ///
1036 + /// The chooser is a described field, so its answer arrives as a parameter,
1037 + /// while the canonical address of a tree is a path segment. This is the join,
1038 + /// and it answers a redirect rather than a page so that there stays exactly one
1039 + /// address for the content: the reader lands on `/tree/{ref}` and that is what
1040 + /// their history, their bookmark and any link they copy will hold.
1041 + ///
1042 + /// The chosen ref is checked against the repository's own refs rather than
1043 + /// against a character rule. Anything the chooser can submit is in that list by
1044 + /// construction, so the check costs nothing a reader will ever meet, and it is
1045 + /// what keeps a hand-written parameter from reaching the `Location` header.
1046 + #[tracing::instrument(skip_all, name = "git::ref_chosen")]
1047 + pub(super) async fn ref_chosen(
1048 + State(db): State<PgPool>,
1049 + State(config): State<Config>,
1050 + MaybeUserVerified(maybe_user): MaybeUserVerified,
1051 + headers: axum::http::HeaderMap,
1052 + Path((owner, repo_name)): Path<(String, String)>,
1053 + ValidatedQuery(choice): ValidatedQuery<RefChoice>,
1054 + ) -> Result<Response> {
1055 + let resolved = resolve_repo(
1056 + &db,
1057 + &config,
1058 + &owner,
1059 + &repo_name,
1060 + maybe_user.as_ref().map(|u| u.id),
1061 + )
1062 + .await?;
1063 +
1064 + let chosen = choice.chosen.clone();
1065 + let known = resolved
1066 + .with_repo(move |gix_repo| {
1067 + Ok(git::list_refs(gix_repo)
1068 + .into_iter()
1069 + .any(|r| r.name == chosen))
1070 + })
1071 + .await?;
1072 +
1073 + if !known {
1074 + return Err(AppError::NotFound);
1075 + }
1076 +
1077 + Ok(crate::helpers::redirect_to(
1078 + &headers,
1079 + &format!("/git/{owner}/{repo_name}/tree/{}", choice.chosen),
1080 + ))
1081 + }
@@ -60,6 +60,10 @@
60 60 "/git/{owner}/{repo}/blame/{ref}/{*path}",
61 61 get(browsing::blame_view),
62 62 )
63 + // The ref chooser's destination. A control's address, not a page's:
64 + // it answers a redirect to the canonical tree URL. See
65 + // `crate::quasi::widgets::git_nav`.
66 + .route("/git/{owner}/{repo}/ref", get(browsing::ref_chosen))
63 67 .route("/git/{owner}/{repo}/tags", get(browsing::tags_tab))
64 68 .route("/git/{owner}/{repo}/replace", get(browsing::replace_tab))
65 69 .route("/git/{owner}/{repo}/notes", get(browsing::notes_tab))
@@ -12,16 +12,15 @@
12 12 <a href="/git/{{ owner }}/{{ repo_name }}">{{ repo_name }}</a>
13 13 </h1>
14 14
15 - <div class="git-ref-bar">
16 - <nav class="git-nav-links">
17 - <a href="/git/{{ owner }}/{{ repo_name }}/tree/{{ current_ref }}">Files</a>
18 - <a href="/git/{{ owner }}/{{ repo_name }}/commits/{{ current_ref }}">Commits</a>
19 - <a href="/git/{{ owner }}/{{ repo_name }}/issues" class="is-selected">Issues ({{ open_issue_count }})</a>
20 - {% if is_owner %}
21 - <a href="/git/{{ owner }}/{{ repo_name }}/settings">Settings</a>
22 - {% endif %}
23 - </nav>
24 - </div>
15 + {{ crate::quasi::widgets::git_nav::html(&crate::quasi::widgets::git_nav::Nav {
16 + owner: owner,
17 + repo: repo_name,
18 + current_ref: current_ref,
19 + active_tab: "issues",
20 + open_issue_count: *open_issue_count,
21 + is_owner: *is_owner,
22 + refs: crate::quasi::widgets::git_nav::NO_REFS,
23 + })|safe }}
25 24
26 25 <div class="issue-detail">
27 26 <div class="issue-detail-header">
@@ -12,16 +12,15 @@
12 12 <a href="/git/{{ owner }}/{{ repo_name }}">{{ repo_name }}</a>
13 13 </h1>
14 14
15 - <div class="git-ref-bar">
16 - <nav class="git-nav-links">
17 - <a href="/git/{{ owner }}/{{ repo_name }}/tree/{{ current_ref }}">Files</a>
18 - <a href="/git/{{ owner }}/{{ repo_name }}/commits/{{ current_ref }}">Commits</a>
19 - <a href="/git/{{ owner }}/{{ repo_name }}/issues" class="is-selected">Issues ({{ open_count }})</a>
20 - {% if is_owner %}
21 - <a href="/git/{{ owner }}/{{ repo_name }}/settings">Settings</a>
22 - {% endif %}
23 - </nav>
24 - </div>
15 + {{ crate::quasi::widgets::git_nav::html(&crate::quasi::widgets::git_nav::Nav {
16 + owner: owner,
17 + repo: repo_name,
18 + current_ref: current_ref,
19 + active_tab: "issues",
20 + open_issue_count: *open_count,
21 + is_owner: *is_owner,
22 + refs: crate::quasi::widgets::git_nav::NO_REFS,
23 + })|safe }}
25 24
26 25 <div class="issue-header">
27 26 <div class="issue-tabs">