gitshark

Clone repository

git clone https://gitshark.ha1nz.de/git/workaround/Gitshark.git
git clone git@gitshark.ha1nz.de:workaround/Gitshark.git

← Commits

✨ (nav): Dismiss the account menu on escape or an outside click

2d5e78d9ca16411d66c351149718aaf79ed21701 · Phillip Souza Furtner · 2026-09-08T12:06:46Z

Changes

4 files changed, +47 -2

MODIFY README.md +1 -1
diff --git a/README.md b/README.md
index ab24a17..55a1f08 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@
24 24 - public-key authentication only; keys managed per user in the UI
25 25 - the server binds an unprivileged port (default 2222); the port shown in URLs is set
26 26 separately via `GITSHARK_SSH_EXTERNAL_PORT` (default 22) to match your host mapping
27 -- Web UI: an auth-aware header nav (a "Log in" button for visitors; for signed-in users a top-level Following link plus an Account dropdown holding Profile, SSH keys, Access tokens, and Logout — a JS-free `<details>` menu), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files; Markdown files render to HTML by default with a Rendered/Code toggle), a rendered README (commonmark-java with GFM tables, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Releases, Issues, Merge requests, plus Settings for the owner) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
27 +- Web UI: an auth-aware header nav (a "Log in" button for visitors; for signed-in users a top-level Following link plus an Account dropdown holding Profile, SSH keys, Access tokens, and Logout — a `<details>` menu that needs no JavaScript and, where JavaScript runs, closes on `Escape` or a click outside), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files; Markdown files render to HTML by default with a Rendered/Code toggle), a rendered README (commonmark-java with GFM tables, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Releases, Issues, Merge requests, plus Settings for the owner) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it or the open account menu, `g h` goes home) — every page works fully without JavaScript
28 28 - Per-repository issues: title, optional description (rendered as Markdown, XSS-safe), per-repo sequential number (`#1`, `#2`, …), an author, and an optional assignee (any local user, set by username; blank clears it); created and managed by the repo owner and collaborators, readable by anyone who can read the repo, via a dedicated "New issue" page; title and description can be edited afterwards via an "Edit issue" page. Issue pages are addressed by number (`…/issues/1`); old UUID URLs redirect permanently to the number form
29 29 - Issues move through a fixed lifecycle (Planned → In development → Done); the repo navigation shows the open (Planned + In development) issue count, and Done issues collapse into an "Archive" section on the issues page
30 30 - Issues auto-close from pushed commit messages, GitHub-style (`close(s|d)`/`fix(es|ed)`/`resolve(s|d)` + `#<number>`, e.g. `fixes #12`), over both HTTP and SSH pushes
MODIFY src/main/resources/META-INF/resources/shark-hotkeys.js +31 -0
diff --git a/src/main/resources/META-INF/resources/shark-hotkeys.js b/src/main/resources/META-INF/resources/shark-hotkeys.js
index ec8362e..07f2ef0 100644
--- a/src/main/resources/META-INF/resources/shark-hotkeys.js
+++ b/src/main/resources/META-INF/resources/shark-hotkeys.js
@@ -3,6 +3,8 @@
3 3 "use strict";
4 4
5 5 var SEQUENCE_TIMEOUT_MS = 1000;
6 + // <details> popovers that must behave like a menu: dismissed by Escape or a click outside.
7 + var MENU_SELECTOR = "details.user-menu";
6 8 var pendingKey = null;
7 9 var pendingTimer = null;
8 10
@@ -18,6 +20,19 @@
18 20 return document.getElementById("hotkey-help");
19 21 }
20 22
23 + function openMenu() {
24 + return document.querySelector(MENU_SELECTOR + "[open]");
25 + }
26 +
27 + function closeMenus(except) {
28 + var menus = document.querySelectorAll(MENU_SELECTOR + "[open]");
29 + for (var i = 0; i < menus.length; i++) {
30 + if (menus[i] !== except) {
31 + menus[i].removeAttribute("open");
32 + }
33 + }
34 + }
35 +
21 36 function clearPending() {
22 37 pendingKey = null;
23 38 if (pendingTimer) {
@@ -34,10 +49,20 @@
34 49 var dialog = helpDialog();
35 50
36 51 if (event.key === "Escape") {
52 + var menu = openMenu();
37 53 if (dialog && dialog.open) {
38 54 dialog.close();
39 55 event.preventDefault();
40 56 }
57 + else if (menu) {
58 + closeMenus();
59 + // focus would otherwise stay on the now-hidden menu item
60 + var summary = menu.querySelector("summary");
61 + if (summary) {
62 + summary.focus();
63 + }
64 + event.preventDefault();
65 + }
41 66 clearPending();
42 67 return;
43 68 }
@@ -63,6 +88,12 @@
63 88 }
64 89 });
65 90
91 + // Any click outside an open menu dismisses it; the menu that was clicked keeps its native toggle.
92 + document.addEventListener("click", function (event) {
93 + var target = event.target;
94 + closeMenus(target.closest ? target.closest(MENU_SELECTOR) : null);
95 + });
96 +
66 97 // Generic dialog opener: [data-open-dialog="id"] shows the matching <dialog> as a modal.
67 98 document.addEventListener("click", function (event) {
68 99 var trigger = event.target.closest("[data-open-dialog]");
MODIFY src/main/resources/templates/layout.html +1 -1
diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html
index 72d9995..eab256a 100644
--- a/src/main/resources/templates/layout.html
+++ b/src/main/resources/templates/layout.html
@@ -42,7 +42,7 @@
42 42 <table>
43 43 <tr><td><kbd>?</kbd></td><td>Show this help</td></tr>
44 44 <tr><td><kbd>g</kbd> <kbd>h</kbd></td><td>Go home</td></tr>
45 - <tr><td><kbd>Esc</kbd></td><td>Close dialog</td></tr>
45 + <tr><td><kbd>Esc</kbd></td><td>Close dialog or menu</td></tr>
46 46 </table>
47 47 </dialog>
48 48 {#insert scripts}{/}
MODIFY src/test/java/de/workaround/web/WebUiTest.java +14 -0
diff --git a/src/test/java/de/workaround/web/WebUiTest.java b/src/test/java/de/workaround/web/WebUiTest.java
index 2955d79..df2a8ae 100644
--- a/src/test/java/de/workaround/web/WebUiTest.java
+++ b/src/test/java/de/workaround/web/WebUiTest.java
@@ -18,6 +18,7 @@
18 18 import static io.restassured.RestAssured.given;
19 19 import static org.hamcrest.CoreMatchers.containsString;
20 20 import static org.hamcrest.CoreMatchers.not;
21 +import static org.junit.jupiter.api.Assertions.assertTrue;
21 22
22 23 @QuarkusTest
23 24 class WebUiTest
@@ -605,6 +606,19 @@
605 606 }
606 607
607 608 @Test
609 + void accountMenuIsDismissedByEscapeAndByClickingOutside()
610 + {
611 + String script = given().when().get("/shark-hotkeys.js")
612 + .then().statusCode(200)
613 + .extract().body().asString();
614 +
615 + assertTrue(script.contains("details.user-menu"),
616 + "the account dropdown must be wired for dismissal in shark-hotkeys.js");
617 + assertTrue(script.contains("closeMenus"),
618 + "shark-hotkeys.js must close open menus (escape handler and outside click)");
619 + }
620 +
621 + @Test
608 622 @TestSecurity(user = "ui-mallory")
609 623 void privateRepositoryHiddenFromStranger() throws Exception
610 624 {

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog