✨ (ui): Add repo tab navigation, restyle remaining pages, self-host fonts
Changes
27 files changed, +451 -38
MODIFY
.github/workflows/native.yml
+2 -2
@@ -15,5 +15,5 @@
15
15
distribution: temurin
16
16
java-version: '21'
17
17
cache: maven
18
- - name: Native build
19
- run: ./mvnw -B package -Dnative -DskipTests -Dquarkus.native.container-build=true
18
+ - name: Native build & integration tests
19
+ run: ./mvnw -B verify -Dnative -Dquarkus.native.container-build=true
MODIFY
README.md
+2 -1
@@ -14,7 +14,7 @@
14
14
- push and private read authenticate with **personal access tokens** (HTTP Basic password)
15
15
- Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
16
16
- public-key authentication only; keys managed per user in the UI
17
-- Web UI: 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), commit log (paginated), branches & tags, one-time handle selection (`/onboarding`), profile settings (`/settings/profile`)
17
+- Web UI: 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), commit log (paginated), branches & tags, one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Repository pages share Files / Commits / Branches tab navigation that preserves the selected ref. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
18
18
- OIDC login (authorization code flow) via `GET /login`; on first login the user account is created without a username and the browser is redirected to `/onboarding`, where the user picks a URL-safe handle (`^[a-z0-9][a-z0-9-]{0,38}$`, unique). The chosen handle — not the OIDC `preferred_username` claim (which is an SPN form in kanidm and not URL-safe) — is used in all repo, SSH, ActivityPub, and webfinger URLs. The `name` claim becomes an editable display name; both can be changed later at `/settings/profile`. A request filter blocks all app pages until a handle is chosen. Logout is local-session only via `POST /logout` (the kanidm provider advertises no `end_session_endpoint`, so RP-Initiated Logout is disabled)
19
19
- Single access policy on all paths: owner read/write, public world-readable, private owner-only
20
20
- **Federation (ForgeFed / ActivityPub)** — *opt-in, off by default.* Public repositories are
@@ -56,6 +56,7 @@
56
56
- The SSH host key is generated on first start and persisted, so the host identity is
57
57
stable across restarts.
58
58
- SSH serves only `git-upload-pack`/`git-receive-pack`; shells and other commands are rejected.
59
+- UI fonts (Space Grotesk, JetBrains Mono) are self-hosted from the application origin — no CDN or other external requests, so the UI works fully offline/air-gapped.
59
60
60
61
## Configuration
61
62
MODIFY
openspec/changes/add-dashboard-pinned-notifications/tasks.md
+1 -1
@@ -37,5 +37,5 @@
37
37
## 6. Verify
38
38
39
39
- [x] 6.1 Run `./mvnw test` — 69/70 green. The one failure (`SettingsPagesTest.anonymousIsRedirectedToLogin`) is a pre-existing Keycloak Dev Service startup flake ("OIDC Server is not available"), unrelated to this change; all new tests pass
40
-- [ ] 6.2 Manual check in `quarkus:dev`: pin/unpin reflects in the pinned section; notifications shows the empty state; full list still lists all visible repos; anonymous `/` unchanged
40
+- [x] 6.2 Manual check in `quarkus:dev`: pin/unpin reflects in the pinned section; notifications shows the empty state; full list still lists all visible repos; anonymous `/` unchanged
41
41
- [x] 6.3 Update `README.md` for the user-facing dashboard/pinning behavior and the new `repository_pins` table
MODIFY
openspec/changes/add-user-onboarding/tasks.md
+1 -1
@@ -32,4 +32,4 @@
32
32
## 6. Verify
33
33
34
34
- [x] 6.1 Run full test suite green; format (`mvn formatter:format` + `mvn impsort:sort`)
35
-- [ ] 6.2 Manual dev check: fresh OIDC subject → onboarding → handle in repo/AP/webfinger URLs; rename via settings; seeded alice unaffected
35
+- [x] 6.2 Manual dev check: fresh OIDC subject → onboarding → handle in repo/AP/webfinger URLs; rename via settings; seeded alice unaffected
MODIFY
openspec/changes/ui-design-system/design.md
+5 -0
@@ -62,6 +62,11 @@
62
62
on every modern OS with zero payload and zero native-image config; revisit only if the rendered result disappoints.
63
63
This satisfies "self-hosted, no CDN" trivially — there are no font requests at all.
64
64
65
+**Addendum (2026-07-07):** the rendered result did disappoint — during implementation the UI adopted Space Grotesk +
66
+JetBrains Mono, initially via Google Fonts CDN, which violated the "no external font requests" requirement. Resolved by
67
+self-hosting both families as variable WOFF2 (latin + latin-ext, ~97 KB total) under `META-INF/resources/fonts/` with
68
+`@font-face` in `shark.css` and OFL license texts alongside. System stacks remain as fallbacks in the font tokens.
69
+
65
70
### D4: Tabs are links, active state from a template parameter
66
71
67
72
The repo templates already render per-section pages; each repository page passes an `activeTab` string into the
MODIFY
openspec/changes/ui-design-system/tasks.md
+9 -9
@@ -15,23 +15,23 @@
15
15
## 3. Restyle templates
16
16
17
17
- [x] 3.1 Restyle `HomeResource/home.html` and `newRepo.html`: repo table with visibility badges, primary button for "new repository", styled form (home additionally got a hero section with the shark logo)
18
-- [ ] 3.2 Restyle `RepositoryResource/overview.html`, `tree.html`, `blob.html`: clone URLs and file content in monospace code blocks, empty-repo setup instructions as styled code block
19
-- [ ] 3.3 Restyle `RepositoryResource/commits.html` and `branches.html`: monospace hashes/ref names, default-branch badge, secondary-button pagination
20
-- [ ] 3.4 Restyle `SettingsResource/keys.html`, `tokens.html`, `tokenCreated.html`: styled forms, danger buttons for delete actions, monospace fingerprints/tokens
18
+- [x] 3.2 Restyle `RepositoryResource/overview.html`, `tree.html`, `blob.html`: clone URLs and file content in monospace code blocks, empty-repo setup instructions as styled code block
19
+- [x] 3.3 Restyle `RepositoryResource/commits.html` and `branches.html`: monospace hashes/ref names, default-branch badge, secondary-button pagination
20
+- [x] 3.4 Restyle `SettingsResource/keys.html`, `tokens.html`, `tokenCreated.html`: styled forms, danger buttons for delete actions, monospace fingerprints/tokens
21
21
22
22
## 4. Repository tab navigation
23
23
24
-- [ ] 4.1 Write failing tests: repo pages render Files/Commits/Branches tabs with correct active tab, and tab links preserve the selected ref
25
-- [ ] 4.2 Pass an `activeTab` parameter from `RepositoryResource` page methods to the templates
26
-- [ ] 4.3 Add the tab markup (shared Qute fragment or per-template) using the `.tabs` component; make 4.1 green
24
+- [x] 4.1 Write failing tests: repo pages render Files/Commits/Branches tabs with correct active tab, and tab links preserve the selected ref
25
+- [x] 4.2 Pass an `activeTab` parameter from `RepositoryResource` page methods to the templates
26
+- [x] 4.3 Add the tab markup (shared Qute fragment or per-template) using the `.tabs` component; make 4.1 green
27
27
28
28
## 5. Hotkeys (progressive enhancement)
29
29
30
30
- [x] 5.1 Create `src/main/resources/META-INF/resources/shark-hotkeys.js`: key map (`?` help, `Escape` close, `g h` home), form-field guard, ~1s sequence timeout per D5; include via `<script defer>` in `layout.html`
31
-- [ ] 5.2 Write test asserting pages render and forms work without JavaScript (server-side rendering unaffected by the script tag)
31
+- [x] 5.2 Write test asserting pages render and forms work without JavaScript (server-side rendering unaffected by the script tag)
32
32
33
33
## 6. Verification
34
34
35
-- [ ] 6.1 Run full test suite via bishbash; all green
36
-- [ ] 6.2 Manual pass over all 11 pages: light theme consistent, tabs/buttons/badges correct, hotkeys work, `?` overlay opens/closes
35
+- [x] 6.1 Run full test suite via bishbash; all green
36
+- [x] 6.2 Manual pass over all 11 pages: light theme consistent, tabs/buttons/badges correct, hotkeys work, `?` overlay opens/closes
37
37
- [ ] 6.3 Verify native image still builds and serves `/shark.css` + `/shark-hotkeys.js` (META-INF/resources inclusion per D6)
MODIFY
src/main/java/de/workaround/web/RepositoryResource.java
+9 -8
@@ -50,16 +50,16 @@
50
50
int branchCount, int tagCount);
51
51
52
52
static native TemplateInstance tree(Repository repo, String ref, String path,
53
- List<GitBrowseService.TreeEntry> entries, List<Crumb> crumbs);
53
+ List<GitBrowseService.TreeEntry> entries, List<Crumb> crumbs, String activeTab);
54
54
55
55
static native TemplateInstance blob(Repository repo, String ref, String path, boolean binary, String content,
56
- String language, List<Crumb> crumbs);
56
+ String language, List<Crumb> crumbs, String activeTab);
57
57
58
58
static native TemplateInstance commits(Repository repo, String ref, List<GitBrowseService.CommitInfo> commits,
59
- int page, int prevPage, int nextPage, int size, boolean hasNext);
59
+ int page, int prevPage, int nextPage, int size, boolean hasNext, String activeTab);
60
60
61
61
static native TemplateInstance branches(Repository repo, List<GitBrowseService.BranchInfo> branches,
62
- List<String> tags);
62
+ List<String> tags, String activeTab, String tabRef);
63
63
}
64
64
65
65
@Inject
@@ -167,7 +167,7 @@
167
167
Repository repo = requireReadable(owner, name);
168
168
Path repoPath = service.repositoryPath(repo);
169
169
return browse.listTree(repoPath, ref, path)
170
- .map(entries -> Templates.tree(repo, ref, path, entries, breadcrumbs(repo, ref, path)))
170
+ .map(entries -> Templates.tree(repo, ref, path, entries, breadcrumbs(repo, ref, path), "files"))
171
171
.orElseGet(() -> blobView(repo, repoPath, ref, path));
172
172
}
173
173
@@ -196,7 +196,7 @@
196
196
.commits(service.repositoryPath(repo), ref, boundedPage, boundedSize)
197
197
.orElseThrow(NotFoundException::new);
198
198
return Templates.commits(repo, ref, commitPage.commits(), boundedPage, boundedPage - 1, boundedPage + 1,
199
- boundedSize, commitPage.hasNext());
199
+ boundedSize, commitPage.hasNext(), "commits");
200
200
}
201
201
202
202
@GET
@@ -205,7 +205,8 @@
205
205
{
206
206
Repository repo = requireReadable(owner, name);
207
207
Path path = service.repositoryPath(repo);
208
- return Templates.branches(repo, browse.branches(path), browse.tags(path));
208
+ String tabRef = browse.isEmpty(path) ? null : browse.defaultBranch(path);
209
+ return Templates.branches(repo, browse.branches(path), browse.tags(path), "branches", tabRef);
209
210
}
210
211
211
212
@POST
@@ -261,7 +262,7 @@
261
262
GitBrowseService.BlobView blob = browse.blob(repoPath, ref, path).orElseThrow(NotFoundException::new);
262
263
String content = blob.binary() ? null : new String(blob.content(), StandardCharsets.UTF_8);
263
264
String language = blob.binary() ? null : highlightLanguage(path);
264
- return Templates.blob(repo, ref, path, blob.binary(), content, language, breadcrumbs(repo, ref, path));
265
+ return Templates.blob(repo, ref, path, blob.binary(), content, language, breadcrumbs(repo, ref, path), "files");
265
266
}
266
267
267
268
// Extension → highlight.js language id. Every value here MUST have a grammar in the bundled highlight assets
ADD
src/main/resources/META-INF/resources/fonts/OFL-JetBrainsMono.txt
+93 -0
@@ -0,0 +1,93 @@
1
+Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
2
+
3
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+This license is copied below, and is also available with a FAQ at:
5
+https://openfontlicense.org
6
+
7
+
8
+-----------------------------------------------------------
9
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+-----------------------------------------------------------
11
+
12
+PREAMBLE
13
+The goals of the Open Font License (OFL) are to stimulate worldwide
14
+development of collaborative font projects, to support the font creation
15
+efforts of academic and linguistic communities, and to provide a free and
16
+open framework in which fonts may be shared and improved in partnership
17
+with others.
18
+
19
+The OFL allows the licensed fonts to be used, studied, modified and
20
+redistributed freely as long as they are not sold by themselves. The
21
+fonts, including any derivative works, can be bundled, embedded,
22
+redistributed and/or sold with any software provided that any reserved
23
+names are not used by derivative works. The fonts and derivatives,
24
+however, cannot be released under any other type of license. The
25
+requirement for fonts to remain under this license does not apply
26
+to any document created using the fonts or their derivatives.
27
+
28
+DEFINITIONS
29
+"Font Software" refers to the set of files released by the Copyright
30
+Holder(s) under this license and clearly marked as such. This may
31
+include source files, build scripts and documentation.
32
+
33
+"Reserved Font Name" refers to any names specified as such after the
34
+copyright statement(s).
35
+
36
+"Original Version" refers to the collection of Font Software components as
37
+distributed by the Copyright Holder(s).
38
+
39
+"Modified Version" refers to any derivative made by adding to, deleting,
40
+or substituting -- in part or in whole -- any of the components of the
41
+Original Version, by changing formats or by porting the Font Software to a
42
+new environment.
43
+
44
+"Author" refers to any designer, engineer, programmer, technical
45
+writer or other person who contributed to the Font Software.
46
+
47
+PERMISSION & CONDITIONS
48
+Permission is hereby granted, free of charge, to any person obtaining
49
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+redistribute, and sell modified and unmodified copies of the Font
51
+Software, subject to the following conditions:
52
+
53
+1) Neither the Font Software nor any of its individual components,
54
+in Original or Modified Versions, may be sold by itself.
55
+
56
+2) Original or Modified Versions of the Font Software may be bundled,
57
+redistributed and/or sold with any software, provided that each copy
58
+contains the above copyright notice and this license. These can be
59
+included either as stand-alone text files, human-readable headers or
60
+in the appropriate machine-readable metadata fields within text or
61
+binary files as long as those fields can be easily viewed by the user.
62
+
63
+3) No Modified Version of the Font Software may use the Reserved Font
64
+Name(s) unless explicit written permission is granted by the corresponding
65
+Copyright Holder. This restriction only applies to the primary font name as
66
+presented to the users.
67
+
68
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+Software shall not be used to promote, endorse or advertise any
70
+Modified Version, except to acknowledge the contribution(s) of the
71
+Copyright Holder(s) and the Author(s) or with their explicit written
72
+permission.
73
+
74
+5) The Font Software, modified or unmodified, in part or in whole,
75
+must be distributed entirely under this license, and must not be
76
+distributed under any other license. The requirement for fonts to
77
+remain under this license does not apply to any document created
78
+using the Font Software.
79
+
80
+TERMINATION
81
+This license becomes null and void if any of the above conditions are
82
+not met.
83
+
84
+DISCLAIMER
85
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+OTHER DEALINGS IN THE FONT SOFTWARE.
ADD
src/main/resources/META-INF/resources/fonts/OFL-SpaceGrotesk.txt
+93 -0
@@ -0,0 +1,93 @@
1
+Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
2
+
3
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+This license is copied below, and is also available with a FAQ at:
5
+http://scripts.sil.org/OFL
6
+
7
+
8
+-----------------------------------------------------------
9
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+-----------------------------------------------------------
11
+
12
+PREAMBLE
13
+The goals of the Open Font License (OFL) are to stimulate worldwide
14
+development of collaborative font projects, to support the font creation
15
+efforts of academic and linguistic communities, and to provide a free and
16
+open framework in which fonts may be shared and improved in partnership
17
+with others.
18
+
19
+The OFL allows the licensed fonts to be used, studied, modified and
20
+redistributed freely as long as they are not sold by themselves. The
21
+fonts, including any derivative works, can be bundled, embedded,
22
+redistributed and/or sold with any software provided that any reserved
23
+names are not used by derivative works. The fonts and derivatives,
24
+however, cannot be released under any other type of license. The
25
+requirement for fonts to remain under this license does not apply
26
+to any document created using the fonts or their derivatives.
27
+
28
+DEFINITIONS
29
+"Font Software" refers to the set of files released by the Copyright
30
+Holder(s) under this license and clearly marked as such. This may
31
+include source files, build scripts and documentation.
32
+
33
+"Reserved Font Name" refers to any names specified as such after the
34
+copyright statement(s).
35
+
36
+"Original Version" refers to the collection of Font Software components as
37
+distributed by the Copyright Holder(s).
38
+
39
+"Modified Version" refers to any derivative made by adding to, deleting,
40
+or substituting -- in part or in whole -- any of the components of the
41
+Original Version, by changing formats or by porting the Font Software to a
42
+new environment.
43
+
44
+"Author" refers to any designer, engineer, programmer, technical
45
+writer or other person who contributed to the Font Software.
46
+
47
+PERMISSION & CONDITIONS
48
+Permission is hereby granted, free of charge, to any person obtaining
49
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+redistribute, and sell modified and unmodified copies of the Font
51
+Software, subject to the following conditions:
52
+
53
+1) Neither the Font Software nor any of its individual components,
54
+in Original or Modified Versions, may be sold by itself.
55
+
56
+2) Original or Modified Versions of the Font Software may be bundled,
57
+redistributed and/or sold with any software, provided that each copy
58
+contains the above copyright notice and this license. These can be
59
+included either as stand-alone text files, human-readable headers or
60
+in the appropriate machine-readable metadata fields within text or
61
+binary files as long as those fields can be easily viewed by the user.
62
+
63
+3) No Modified Version of the Font Software may use the Reserved Font
64
+Name(s) unless explicit written permission is granted by the corresponding
65
+Copyright Holder. This restriction only applies to the primary font name as
66
+presented to the users.
67
+
68
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+Software shall not be used to promote, endorse or advertise any
70
+Modified Version, except to acknowledge the contribution(s) of the
71
+Copyright Holder(s) and the Author(s) or with their explicit written
72
+permission.
73
+
74
+5) The Font Software, modified or unmodified, in part or in whole,
75
+must be distributed entirely under this license, and must not be
76
+distributed under any other license. The requirement for fonts to
77
+remain under this license does not apply to any document created
78
+using the Font Software.
79
+
80
+TERMINATION
81
+This license becomes null and void if any of the above conditions are
82
+not met.
83
+
84
+DISCLAIMER
85
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+OTHER DEALINGS IN THE FONT SOFTWARE.
ADD
src/main/resources/META-INF/resources/fonts/README.md
+10 -0
@@ -0,0 +1,10 @@
1
+# Vendored fonts
2
+
3
+Variable WOFF2 fonts self-hosted so the UI makes no external requests (see openspec `ui-design-system`, design D3).
4
+
5
+| Family | Files | Version | Source | License |
6
+|---|---|---|---|---|
7
+| Space Grotesk | `space-grotesk-latin.woff2`, `space-grotesk-latin-ext.woff2` | v22 (Google Fonts API) | https://github.com/floriankarsten/space-grotesk | [OFL-SpaceGrotesk.txt](OFL-SpaceGrotesk.txt) |
8
+| JetBrains Mono | `jetbrains-mono-latin.woff2`, `jetbrains-mono-latin-ext.woff2` | v24 (Google Fonts API) | https://github.com/JetBrains/JetBrainsMono | [OFL-JetBrainsMono.txt](OFL-JetBrainsMono.txt) |
9
+
10
+Downloaded 2026-07-07 from fonts.gstatic.com (latin + latin-ext subsets, variable weight axes). `@font-face` declarations live in `../shark.css`.
ADD
src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin-ext.woff2
+0 -0
0
0
Binary files differ
ADD
src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin.woff2
+0 -0
0
0
Binary files differ
ADD
src/main/resources/META-INF/resources/fonts/space-grotesk-latin-ext.woff2
+0 -0
0
0
Binary files differ
ADD
src/main/resources/META-INF/resources/fonts/space-grotesk-latin.woff2
+0 -0
0
0
Binary files differ
MODIFY
src/main/resources/META-INF/resources/shark.css
+43 -1
@@ -1,5 +1,42 @@
1
1
/* git-shark design system — warm canvas, deep-teal accent, Space Grotesk + JetBrains Mono */
2
2
3
+/* self-hosted variable fonts (no CDN requests; native image ships them from META-INF/resources) */
4
+@font-face {
5
+ font-family: "Space Grotesk";
6
+ font-style: normal;
7
+ font-weight: 300 700;
8
+ font-display: swap;
9
+ src: url(/fonts/space-grotesk-latin.woff2) format('woff2');
10
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
11
+}
12
+
13
+@font-face {
14
+ font-family: "Space Grotesk";
15
+ font-style: normal;
16
+ font-weight: 300 700;
17
+ font-display: swap;
18
+ src: url(/fonts/space-grotesk-latin-ext.woff2) format('woff2');
19
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
20
+}
21
+
22
+@font-face {
23
+ font-family: "JetBrains Mono";
24
+ font-style: normal;
25
+ font-weight: 100 800;
26
+ font-display: swap;
27
+ src: url(/fonts/jetbrains-mono-latin.woff2) format('woff2');
28
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
29
+}
30
+
31
+@font-face {
32
+ font-family: "JetBrains Mono";
33
+ font-style: normal;
34
+ font-weight: 100 800;
35
+ font-display: swap;
36
+ src: url(/fonts/jetbrains-mono-latin-ext.woff2) format('woff2');
37
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
38
+}
39
+
3
40
:root {
4
41
/* surfaces */
5
42
--canvas: oklch(0.985 0.004 80);
@@ -713,12 +750,17 @@
713
750
border-radius: var(--radius);
714
751
}
715
752
716
-td.actions {
753
+td.actions, th.actions {
717
754
width: 1%;
718
755
white-space: nowrap;
719
756
text-align: right;
720
757
}
721
758
759
+.pagination {
760
+ display: flex;
761
+ gap: var(--s2);
762
+}
763
+
722
764
td.actions .btn {
723
765
font-size: 0.8rem;
724
766
}
MODIFY
src/main/resources/templates/RepositoryResource/blob.html
+2 -1
@@ -1,6 +1,7 @@
1
1
{#include layout}
2
2
{#title}{path} at {ref} – {repo.name}{/title}
3
3
<h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4
+{#include RepositoryResource/tabs repo=repo tabRef=ref activeTab=activeTab /}
4
5
<nav class="breadcrumb" aria-label="Path">
5
6
{#for crumb in crumbs}
6
7
{#if crumb.href}<a href="{crumb.href}">{crumb.label}</a>{#else}<span class="current">{crumb.label}</span>{/if}
@@ -8,7 +9,7 @@
8
9
{/for}
9
10
</nav>
10
11
{#if binary}
11
-<p>Binary file. <a href="/repos/{repo.owner.username}/{repo.name}/raw/{ref}/{path}">Download</a></p>
12
+<p>Binary file. <a class="btn btn-secondary" href="/repos/{repo.owner.username}/{repo.name}/raw/{ref}/{path}">Download</a></p>
12
13
{#else if language}
13
14
<pre><code class="language-{language}">{content}</code></pre>
14
15
{#else}
MODIFY
src/main/resources/templates/RepositoryResource/branches.html
+3 -1
@@ -1,13 +1,15 @@
1
1
{#include layout}
2
2
{#title}Branches – {repo.name}{/title}
3
3
<h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4
+<p>Branches and tags</p>
5
+{#include RepositoryResource/tabs repo=repo tabRef=tabRef activeTab=activeTab /}
4
6
<h2>Branches</h2>
5
7
<table>
6
8
<tr><th>Branch</th><th></th></tr>
7
9
{#for branch in branches}
8
10
<tr>
9
11
<td><a href="/repos/{repo.owner.username}/{repo.name}/tree/{branch.name}/">{branch.name}</a></td>
10
- <td>{#if branch.defaultBranch}default{/if}</td>
12
+ <td>{#if branch.defaultBranch}<span class="badge badge-default">default</span>{/if}</td>
11
13
</tr>
12
14
{/for}
13
15
</table>
MODIFY
src/main/resources/templates/RepositoryResource/commits.html
+5 -4
@@ -1,7 +1,8 @@
1
1
{#include layout}
2
2
{#title}Commits at {ref} – {repo.name}{/title}
3
3
<h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4
-<h2>Commits on <code>{ref}</code></h2>
4
+<p>Commits on <code>{ref}</code></p>
5
+{#include RepositoryResource/tabs repo=repo tabRef=ref activeTab=activeTab /}
5
6
<table>
6
7
<tr><th>Commit</th><th>Message</th><th>Author</th><th>Date</th></tr>
7
8
{#for commit in commits}
@@ -13,12 +14,12 @@
13
14
</tr>
14
15
{/for}
15
16
</table>
16
-<p>
17
+<p class="pagination">
17
18
{#if page > 0}
18
- <a href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={prevPage}&size={size}">Newer</a>
19
+ <a class="btn btn-secondary" href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={prevPage}&size={size}">Newer</a>
19
20
{/if}
20
21
{#if hasNext}
21
- <a href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={nextPage}&size={size}">Older</a>
22
+ <a class="btn btn-secondary" href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={nextPage}&size={size}">Older</a>
22
23
{/if}
23
24
</p>
24
25
{/include}
ADD
src/main/resources/templates/RepositoryResource/tabs.html
+7 -0
@@ -0,0 +1,7 @@
1
+{#if tabRef}
2
+<nav class="tabs">
3
+ <a class="tab{#if activeTab == 'files'} active{/if}" href="/repos/{repo.owner.username}/{repo.name}/tree/{tabRef}/">Files</a>
4
+ <a class="tab{#if activeTab == 'commits'} active{/if}" href="/repos/{repo.owner.username}/{repo.name}/commits/{tabRef}">Commits</a>
5
+ <a class="tab{#if activeTab == 'branches'} active{/if}" href="/repos/{repo.owner.username}/{repo.name}/branches">Branches</a>
6
+</nav>
7
+{/if}
MODIFY
src/main/resources/templates/RepositoryResource/tree.html
+1 -0
@@ -1,6 +1,7 @@
1
1
{#include layout}
2
2
{#title}{path ?: '/'} at {ref} – {repo.name}{/title}
3
3
<h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4
+{#include RepositoryResource/tabs repo=repo tabRef=ref activeTab=activeTab /}
4
5
<nav class="breadcrumb" aria-label="Path">
5
6
{#for crumb in crumbs}
6
7
{#if crumb.href}<a href="{crumb.href}">{crumb.label}</a>{#else}<span class="current">{crumb.label}</span>{/if}
MODIFY
src/main/resources/templates/SettingsResource/keys.html
+2 -2
@@ -5,12 +5,12 @@
5
5
<p class="error">{error}</p>
6
6
{/if}
7
7
<table>
8
- <tr><th>Title</th><th>Fingerprint</th><th></th></tr>
8
+ <tr><th>Title</th><th>Fingerprint</th><th class="actions"></th></tr>
9
9
{#for key in keys}
10
10
<tr>
11
11
<td>{key.title}</td>
12
12
<td><code>{key.fingerprint}</code></td>
13
- <td>
13
+ <td class="actions">
14
14
<form class="inline" method="post" action="/settings/keys/{key.id}/delete">
15
15
<button class="btn btn-danger btn-sm">Remove</button>
16
16
</form>
MODIFY
src/main/resources/templates/SettingsResource/tokenCreated.html
+2 -2
@@ -2,7 +2,7 @@
2
2
{#title}Token created – git-shark{/title}
3
3
<h1>Token created</h1>
4
4
<p>Copy your new access token now — it will not be shown again:</p>
5
-<p><code>{plaintext}</code></p>
5
+<pre><code>{plaintext}</code></pre>
6
6
<p>Use it as the password for Git over HTTPS.</p>
7
-<p><a href="/settings/tokens">Back to tokens</a></p>
7
+<p><a class="btn btn-secondary" href="/settings/tokens">Back to tokens</a></p>
8
8
{/include}
MODIFY
src/main/resources/templates/SettingsResource/tokens.html
+2 -2
@@ -2,13 +2,13 @@
2
2
{#title}Access tokens – git-shark{/title}
3
3
<h1>Access tokens</h1>
4
4
<table>
5
- <tr><th>Label</th><th>Created</th><th>Last used</th><th></th></tr>
5
+ <tr><th>Label</th><th>Created</th><th>Last used</th><th class="actions"></th></tr>
6
6
{#for token in tokens}
7
7
<tr>
8
8
<td>{token.label}</td>
9
9
<td>{token.createdAt}</td>
10
10
<td>{token.lastUsed ?: 'never'}</td>
11
- <td>
11
+ <td class="actions">
12
12
<form class="inline" method="post" action="/settings/tokens/{token.id}/revoke">
13
13
<button class="btn btn-danger btn-sm">Revoke</button>
14
14
</form>
MODIFY
src/main/resources/templates/layout.html
+0 -3
@@ -5,9 +5,6 @@
5
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
6
<title>{#insert title}git-shark{/}</title>
7
7
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11
8
<link rel="stylesheet" href="/shark.css">
12
9
<script defer src="/shark-hotkeys.js"></script>
13
10
</head>
MODIFY
src/test/java/de/workaround/SmokeIT.java
+15 -0
@@ -52,6 +52,21 @@
52
52
}
53
53
54
54
@Test
55
+ void designSystemAssetsAreServed()
56
+ {
57
+ given()
58
+ .when().get("/shark.css")
59
+ .then()
60
+ .statusCode(200)
61
+ .body(org.hamcrest.CoreMatchers.containsString(":root"));
62
+
63
+ given()
64
+ .when().get("/shark-hotkeys.js")
65
+ .then()
66
+ .statusCode(200);
67
+ }
68
+
69
+ @Test
55
70
void sshPortPresentsSshBanner() throws Exception
56
71
{
57
72
int port = Integer.getInteger("gitshark.ssh.port", 2222);
MODIFY
src/test/java/de/workaround/web/DesignSystemTest.java
+70 -0
@@ -78,6 +78,76 @@
78
78
.body(containsString("/shark-hotkeys.js"));
79
79
}
80
80
81
+ @Test
82
+ void branchesPageMarksDefaultBranchWithBadge() throws Exception
83
+ {
84
+ User owner = persistUser("ds-badge-" + unique());
85
+ Repository repo = service.create(owner, "ds-branched", Repository.Visibility.PUBLIC, null);
86
+ de.workaround.git.GitTestSeeder.seed(service.repositoryPath(repo),
87
+ java.util.Map.of("a.txt", "a\n".getBytes(java.nio.charset.StandardCharsets.UTF_8)));
88
+
89
+ given().when().get("/repos/" + owner.username + "/ds-branched/branches")
90
+ .then().statusCode(200)
91
+ .body(containsString("badge badge-default"));
92
+ }
93
+
94
+ @Test
95
+ void commitPaginationUsesSecondaryButtons() throws Exception
96
+ {
97
+ User owner = persistUser("ds-pager-" + unique());
98
+ Repository repo = service.create(owner, "ds-paged", Repository.Visibility.PUBLIC, null);
99
+ de.workaround.git.GitTestSeeder.seed(service.repositoryPath(repo),
100
+ java.util.Map.of("a.txt", "a\n".getBytes(java.nio.charset.StandardCharsets.UTF_8)), 3);
101
+
102
+ String base = "/repos/" + owner.username + "/ds-paged/commits/main";
103
+
104
+ given().when().get(base + "?size=2")
105
+ .then().statusCode(200)
106
+ .body(containsString("class=\"btn btn-secondary\""))
107
+ .body(containsString("Older"));
108
+
109
+ given().when().get(base + "?size=2&page=1")
110
+ .then().statusCode(200)
111
+ .body(containsString("class=\"btn btn-secondary\""))
112
+ .body(containsString("Newer"));
113
+ }
114
+
115
+ @Test
116
+ @io.quarkus.test.security.TestSecurity(user = "ds-settings")
117
+ void settingsPagesUseDesignSystemActionColumns()
118
+ {
119
+ persistUser("ds-settings");
120
+
121
+ given().when().get("/settings/keys")
122
+ .then().statusCode(200)
123
+ .body(containsString("class=\"btn btn-primary\""))
124
+ .body(containsString("class=\"actions\""));
125
+
126
+ given().when().get("/settings/tokens")
127
+ .then().statusCode(200)
128
+ .body(containsString("class=\"btn btn-primary\""))
129
+ .body(containsString("class=\"actions\""));
130
+ }
131
+
132
+ @Test
133
+ void fontsAreSelfHostedWithoutCdnRequests()
134
+ {
135
+ given().when().get("/")
136
+ .then().statusCode(200)
137
+ .body(not(containsString("fonts.googleapis.com")))
138
+ .body(not(containsString("fonts.gstatic.com")));
139
+
140
+ given().when().get("/shark.css")
141
+ .then().statusCode(200)
142
+ .body(containsString("@font-face"))
143
+ .body(containsString("/fonts/"));
144
+
145
+ given().when().get("/fonts/space-grotesk-latin.woff2")
146
+ .then().statusCode(200);
147
+ given().when().get("/fonts/jetbrains-mono-latin.woff2")
148
+ .then().statusCode(200);
149
+ }
150
+
81
151
private static String unique()
82
152
{
83
153
return UUID.randomUUID().toString().substring(0, 8);
MODIFY
src/test/java/de/workaround/web/WebUiTest.java
+74 -0
@@ -268,6 +268,80 @@
268
268
}
269
269
270
270
@Test
271
+ void repositoryPagesRenderTabsWithActiveSection() throws Exception
272
+ {
273
+ User owner = persistUser("ui-tabs-" + unique());
274
+ Repository repo = service.create(owner, "tabbed", Repository.Visibility.PUBLIC, null);
275
+ GitTestSeeder.seed(service.repositoryPath(repo),
276
+ Map.of("a.txt", "a\n".getBytes(StandardCharsets.UTF_8)));
277
+
278
+ String base = "/repos/" + owner.username + "/tabbed";
279
+
280
+ given().when().get(base + "/tree/main")
281
+ .then().statusCode(200)
282
+ .body(containsString("class=\"tabs\""))
283
+ .body(containsString("class=\"tab active\" href=\"" + base + "/tree/main/\">Files"))
284
+ .body(containsString("class=\"tab\" href=\"" + base + "/commits/main\">Commits"))
285
+ .body(containsString("class=\"tab\" href=\"" + base + "/branches\">Branches"));
286
+
287
+ given().when().get(base + "/tree/main/a.txt")
288
+ .then().statusCode(200)
289
+ .body(containsString("class=\"tab active\" href=\"" + base + "/tree/main/\">Files"));
290
+
291
+ given().when().get(base + "/commits/main")
292
+ .then().statusCode(200)
293
+ .body(containsString("class=\"tab active\" href=\"" + base + "/commits/main\">Commits"));
294
+
295
+ given().when().get(base + "/branches")
296
+ .then().statusCode(200)
297
+ .body(containsString("class=\"tab active\" href=\"" + base + "/branches\">Branches"));
298
+ }
299
+
300
+ @Test
301
+ void tabLinksPreserveSelectedRef() throws Exception
302
+ {
303
+ User owner = persistUser("ui-kate-" + unique());
304
+ Repository repo = service.create(owner, "refkeep", Repository.Visibility.PUBLIC, null);
305
+ GitTestSeeder.seed(service.repositoryPath(repo),
306
+ Map.of("a.txt", "a\n".getBytes(StandardCharsets.UTF_8)));
307
+ try (org.eclipse.jgit.api.Git git = org.eclipse.jgit.api.Git.open(service.repositoryPath(repo).toFile()))
308
+ {
309
+ git.branchCreate().setName("feature").setStartPoint("main").call();
310
+ }
311
+
312
+ String base = "/repos/" + owner.username + "/refkeep";
313
+ given().when().get(base + "/tree/feature")
314
+ .then().statusCode(200)
315
+ .body(containsString("class=\"tab active\" href=\"" + base + "/tree/feature/\">Files"))
316
+ .body(containsString("href=\"" + base + "/commits/feature\">Commits"));
317
+ }
318
+
319
+ @Test
320
+ @io.quarkus.test.security.TestSecurity(user = "ui-nojs")
321
+ void formsWorkWithoutJavaScriptViaStandardPost() throws Exception
322
+ {
323
+ persistUser("ui-nojs");
324
+ String name = "nojs-" + unique();
325
+
326
+ // a plain form-encoded POST (what a browser sends with JS disabled) must complete the flow
327
+ given().contentType("application/x-www-form-urlencoded")
328
+ .formParam("name", name)
329
+ .formParam("visibility", "PUBLIC")
330
+ .formParam("description", "created without JS")
331
+ .when().redirects().follow(false).post("/repos")
332
+ .then().statusCode(org.hamcrest.Matchers.anyOf(
333
+ org.hamcrest.Matchers.is(302), org.hamcrest.Matchers.is(303)));
334
+
335
+ given().when().get("/repos/ui-nojs/" + name)
336
+ .then().statusCode(200);
337
+
338
+ // scripting is enhancement-only: loaded deferred, no inline handlers required for rendering
339
+ given().when().get("/")
340
+ .then().statusCode(200)
341
+ .body(containsString("<script defer src=\"/shark-hotkeys.js\">"));
342
+ }
343
+
344
+ @Test
271
345
void branchesPageMarksDefaultBranch() throws Exception
272
346
{
273
347
User owner = persistUser("ui-fred-" + unique());