UI: das Repository wird in der Seitenüberschrift per Drop-Down gewählt

Die Link-Leiste neben dem Menü entfällt. An ihrer Stelle steht dort, wo
bisher der Repository-Name stand, eine Auswahlliste der bedienten
Repositories; werkator.js hängt die aktuelle Ansicht (branches, history,
current) an, damit der Wechsel auf derselben Seite bleibt. Mit nur einem
Repository bleibt der Name wie bisher reiner Text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-09-03 15:26:29 +02:00
co-authored by Claude Fable 5.1
parent e61adfbbaa
commit fe11a339ad
7 changed files with 46 additions and 15 deletions
+6
View File
@@ -57,6 +57,11 @@ main { width: min(1180px, calc(100% - 32px)); margin: 32px auto; }
h1 { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; font-size: 28px; font-weight: 700; }
h1 img { width: 32px; height: 32px; flex: none; }
h1 .repo-name { color: var(--muted); font-size: 18px; font-weight: 400; align-self: flex-end; }
/* the switcher takes the repository name's place and dresses like it: no box until hovered */
h1 .repo-select { align-self: flex-end; margin-bottom: 3px; padding: 2px 4px; border: 1px solid transparent; border-radius: 6px;
background: transparent; color: var(--muted); font: inherit; font-size: 18px; font-weight: 400; cursor: pointer; }
h1 .repo-select:hover, h1 .repo-select:focus { border-color: var(--border); background: var(--panel); }
h1 .repo-select option { color: var(--text); background: var(--panel); }
h2 { margin: 20px 0 10px; font-size: 18px; }
.title-home { display: inline-flex; flex: none; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
@@ -163,6 +168,7 @@ th.num { font-family: inherit; font-size: 12px; }
h1 .title-home { grid-column: 1; grid-row: 1 / span 2; align-self: center; }
h1 .title-text { grid-column: 2; grid-row: 1; align-self: end; }
h1 .repo-name { grid-column: 2; grid-row: 2; align-self: start; font-size: 15px; overflow-wrap: anywhere; }
h1 .repo-select { grid-column: 2; grid-row: 2; align-self: start; justify-self: start; margin: 0; padding: 0 4px; font-size: 15px; }
.view-row { flex-wrap: wrap; }
.view-toggle a, .view-toggle span { min-width: 0; padding: 6px 9px; font-size: 13px; }
/* the live indicator collapses to a colored state dot so it cannot squeeze the menu;
+22
View File
@@ -736,12 +736,34 @@ function initReloadButton() {
});
}
// ---- repository switcher -------------------------------------------------------
/**
* Switching the repository keeps the view: an option's value is the repository root
* (`/repos/<name>`), and the view this page shows is appended to it. A page that only
* exists for one repository's build — an artifact index — falls back to that
* repository's latest builds.
*/
function initRepoSelect() {
const select = document.getElementById("repo-select");
if (!select) {
return;
}
const path = window.location.pathname;
const view = repoBase && path.startsWith(repoBase) ? path.slice(repoBase.length) : path;
const keptView = ["/branches", "/history", "/current"].includes(view) ? view : "";
select.addEventListener("change", () => {
window.location.href = select.value + keptView;
});
}
// ---- page wiring ---------------------------------------------------------------
initBuildsTable();
initCurrentBuilds();
initSystemTable();
initReloadButton();
initRepoSelect();
// pages with a poller update the banner from their own tick; the static ones ask once
if (!refreshNow) {
refreshWatcherBanner();
+9 -9
View File
@@ -16,7 +16,15 @@
<h1 th:fragment="header(title)">
<a class="title-home" th:href="${homeUrl}" href="/" aria-label="Open latest builds"><img src="/favicon.svg" alt=""></a>
<span class="title-text" th:text="${title}">Latest Builds</span>
<span class="repo-name" th:unless="${#strings.isEmpty(repoName)}" th:text="${repoName}">owner/repo</span>
<!-- The repository switcher (ADR 0009): what makes several repositories one UI. It sits
where the repository name sits, because it *is* the repository name — the page's
subject, not a navigation target. Its option values are the repository roots;
werkator.js appends the current view, so switching keeps the reader on the same page.
With a single served repository there is nothing to switch, and the name is plain text. -->
<select th:if="${multiRepo}" id="repo-select" class="repo-select" aria-label="Repository">
<option th:each="r : ${repos}" th:value="${r.url}" th:selected="${r.current}" th:text="${r.name}">werkator</option>
</select>
<span class="repo-name" th:unless="${multiRepo} or ${#strings.isEmpty(repoName)}" th:text="${repoName}">owner/repo</span>
</h1>
<th:block th:fragment="nav(view)">
@@ -32,14 +40,6 @@
<span th:if="${view == 'system'}">System</span>
<a th:unless="${view == 'system'}" href="/system">System</a>
</nav>
<!-- The repository switcher (ADR 0009): what makes several repositories one UI.
With a single served repository there is nothing to switch, and the block is absent. -->
<nav class="repo-switch" th:if="${multiRepo}">
<th:block th:each="r : ${repos}">
<span th:if="${r.current}" th:text="${r.name}" class="repo-current">werkator</span>
<a th:unless="${r.current}" th:href="${r.url}" th:text="${r.name}">other</a>
</th:block>
</nav>
<span class="view-row-actions">
<span id="live-indicator" class="status status-unknown" title="live-update state">static</span>
<button id="reload-button" class="reload-button" type="button" title="Reload view" aria-label="Reload view"></button>
@@ -169,7 +169,8 @@ class UiControllerTest : FunSpec() {
.andExpect(content().string(containsString("""href="/branches"""")))
// Thymeleaf drops an attribute whose value is empty, and werkator.js falls back to ""
.andExpect(content().string(containsString("""<meta name="werkator-repo-base">""")))
.andExpect(content().string(not(containsString("""class="repo-switch""""))))
.andExpect(content().string(not(containsString("""class="repo-select""""))))
.andExpect(content().string(containsString("""class="repo-name"""")))
}
test("with several served repositories every link names its repository and the switcher appears") {
@@ -184,8 +185,10 @@ class UiControllerTest : FunSpec() {
.andExpect(content().string(containsString("""href="/repos/test/branches"""")))
.andExpect(content().string(containsString("""data-api="/api/repos/test/builds/latest"""")))
.andExpect(content().string(containsString("""<meta name="werkator-repo-base" content="/repos/test">""")))
.andExpect(content().string(containsString("""class="repo-switch"""")))
.andExpect(content().string(containsString("""href="/repos/other"""")))
.andExpect(content().string(containsString("""class="repo-select"""")))
.andExpect(content().string(containsString("""<option value="/repos/test" selected="selected">test</option>""")))
.andExpect(content().string(containsString("""<option value="/repos/other">other</option>""")))
.andExpect(content().string(not(containsString("""class="repo-name""""))))
}
test("a page of a repository this instance does not serve answers 404") {