๐ (layout): Keep the header gutter the same on every page
Changes
5 files changed, +43 -20
MODIFY
README.md
+1 -1
@@ -58,7 +58,7 @@
58
58
- Line-level review comments on a merge request's diff: any authenticated user who can read the repository can comment on a specific diff line (added, deleted, or context) from the merge-request detail page; comments render inline beneath the line they anchor to. A comment can be deleted by its author, the repository owner, or a collaborator. Comments are anchored to a file plus the diff line's old/new line numbers and must land on a line that's part of the current diff. Hovering a commentable line reveals a comment icon on the right; clicking it opens the form inline โ a progressive-enhancement disclosure that works without JavaScript
59
59
- 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 across users and organisations). 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. The code-flow callback is pinned to `/login` (`redirect-path` + `restore-path-after-redirect`, so strict-`redirect_uri` IdPs like kanidm register one URI) and after login the user lands back on the page they came from โ the header's Log in button carries `?redirect=<current page>` (local paths only, open-redirect guarded). Expired ID tokens are refreshed silently with the refresh token (`refresh-expired=true`, 60 s proactive skew, session cookie usable 12 h past expiry) instead of logging the user out. Logout is local-session only via `POST /logout` (the kanidm provider advertises no `end_session_endpoint`, so RP-Initiated Logout is disabled)
60
60
- Profile pictures: users can upload a PNG/JPEG/GIF/WebP avatar (โค 2 MB, content-type and magic bytes both validated) at `/settings/profile`, stored on the filesystem keyed by user UUID and served publicly at `GET /users/{username}/avatar`; shown wherever a local user is rendered (header nav, repo lists, repo sidebar, issue/MR/comment authors) via a reusable Qute avatar tag, removable, and falling back to an initials badge when absent. Git commit authors and remote federation actors are not local users and keep their existing pseudo-avatars
61
-- Per-user content width setting (Appearance section on `/settings/profile`): **Full** (default โ repository pages span the entire screen, other pages cap at 1400px), **Comfortable (fixed 1400px)**, or **Compact (fixed 1120px)** on every page; the width also aligns the header nav content (the bar background stays edge-to-edge), and windows narrower than the cap just fill the screen. Guide: [profile settings](docs/users/profile.md)
61
+- Per-user content width setting (Appearance section on `/settings/profile`): **Full** (default โ repository pages span the entire screen, other pages cap at 1400px), **Comfortable (fixed 1400px)**, or **Compact (fixed 1120px)** on every page; the header bar keeps a fixed gutter regardless of the preset, and windows narrower than the cap just fill the screen. Guide: [profile settings](docs/users/profile.md)
62
62
- **Collaborators** โ the repository owner can grant other local users read+write access
63
63
(one flat role) on a per-repository settings page (`โฆ/settings/collaborators`, reached from a
64
64
"Manage collaborators" link on the owner-only repository Settings page). Collaborators can read and push โ UI, HTTP, and SSH alike โ even
MODIFY
docs/users/profile.md
+6 -6
@@ -32,12 +32,12 @@
32
32
same fixed pixel column, so the width doesn't shift around when you resize the
33
33
window or navigate. Full is uniform per page type instead โ repository pages
34
34
get the entire screen (diffs and file trees benefit most), everything else the
35
-centered 1400px column. Whatever width the current page uses, the header bar's
36
-content (logo, navigation, account menu) aligns with the same column while the
37
-bar's background still spans the full screen; on windows narrower than the cap
38
-the content simply fills the screen. The setting applies as soon as you save
39
-and sticks across sessions (it's stored on your account, not in the browser).
40
-Visitors who aren't logged in always get the Full layout.
35
+centered 1400px column. The header bar is not affected: its content (logo,
36
+navigation, account menu) keeps the same gutter on every page, so the bar never
37
+shifts as you navigate. On windows narrower than the cap the content simply
38
+fills the screen. The setting applies as soon as you save and sticks across
39
+sessions (it's stored on your account, not in the browser). Visitors who aren't
40
+logged in always get the Full layout.
41
41
42
42
---
43
43
MODIFY
src/main/java/de/workaround/account/CurrentUser.java
+2 -2
@@ -47,8 +47,8 @@
47
47
}
48
48
49
49
/**
50
- * CSS class for the user's content-width preset, applied on {@code <body>} by the layout so
51
- * header nav and main column share one width. Empty for anonymous visitors and the FULL default.
50
+ * CSS class for the user's content-width preset, applied on {@code <body>} by the layout and
51
+ * driving the main column's width. Empty for anonymous visitors and the FULL default.
52
52
*/
53
53
public String contentWidthClass()
54
54
{
MODIFY
src/main/resources/META-INF/resources/shark.css
+9 -9
@@ -94,12 +94,12 @@
94
94
}
95
95
96
96
body {
97
- /* Per-user content width preset (settings > profile > appearance): one shared width for the
98
- header nav content and the page's main column. --preset-w is what the user picked (Full =
99
- the whole screen; the other presets are fixed px caps so the column stays put while the
100
- window is resized โ narrower windows simply fill the viewport). Non-repo pages additionally
101
- cap at a comfortable 1400px; repo pages (the :has override below) get the raw preset width,
102
- so Full there means the entire screen for diffs and file trees. */
97
+ /* Per-user content width preset (settings > profile > appearance) for the page's main column;
98
+ the header bar keeps its own fixed gutter. --preset-w is what the user picked (Full = the
99
+ whole screen; the other presets are fixed px caps so the column stays put while the window
100
+ is resized โ narrower windows simply fill the viewport). Non-repo pages additionally cap at
101
+ a comfortable 1400px; repo pages (the :has override below) get the raw preset width, so
102
+ Full there means the entire screen for diffs and file trees. */
103
103
--preset-w: 100%;
104
104
--content-w: min(var(--preset-w), 1400px);
105
105
margin: 0;
@@ -180,9 +180,9 @@
180
180
display: flex;
181
181
align-items: center;
182
182
gap: var(--s4);
183
- /* bar background spans the viewport, its content aligns with the preset content column;
184
- max() keeps the normal gutter when the window is narrower than the preset cap */
185
- padding: var(--s3) max(var(--s5), calc((100% - var(--content-w)) / 2 + var(--s5)));
183
+ /* a fixed gutter on purpose: tracking --content-w made the bar jump between page types,
184
+ because repo pages opt out of the 1400px cap */
185
+ padding: var(--s3) var(--s5);
186
186
background: var(--surface);
187
187
border-bottom: 1px solid var(--border);
188
188
}
MODIFY
src/test/java/de/workaround/web/ContentWidthTest.java
+25 -2
@@ -11,6 +11,8 @@
11
11
import static io.restassured.RestAssured.given;
12
12
import static org.hamcrest.CoreMatchers.containsString;
13
13
import static org.hamcrest.CoreMatchers.not;
14
+import static org.junit.jupiter.api.Assertions.assertFalse;
15
+import static org.junit.jupiter.api.Assertions.assertTrue;
14
16
15
17
@QuarkusTest
16
18
class ContentWidthTest
@@ -48,7 +50,7 @@
48
50
.when().get("/explore")
49
51
.then()
50
52
.statusCode(200)
51
- // the preset class sits on <body> so header nav and content scale together
53
+ // the preset class sits on <body> and drives the main column's width
52
54
.body(containsString("<body class=\"width-compact\">"));
53
55
54
56
given()
@@ -90,7 +92,7 @@
90
92
.when().get("/shark.css")
91
93
.then()
92
94
.statusCode(200)
93
- // presets live on <body> and drive one shared width variable for header and main;
95
+ // presets live on <body> and drive one shared width variable for the main column;
94
96
// fixed px caps so the column doesn't shift while resizing the window
95
97
.body(containsString("body.width-comfortable"))
96
98
.body(containsString("body.width-compact"))
@@ -101,6 +103,27 @@
101
103
.body(containsString("body:has(.repo-layout)"));
102
104
}
103
105
106
+ @Test
107
+ void headerBarKeepsTheSameGutterOnEveryPage() throws Exception
108
+ {
109
+ String css = given()
110
+ .when().get("/shark.css")
111
+ .then()
112
+ .statusCode(200)
113
+ .extract().body().asString();
114
+
115
+ int start = css.indexOf("header.site {");
116
+ assertTrue(start >= 0, "header.site rule missing from the stylesheet");
117
+ String rule = css.substring(start, css.indexOf('}', start));
118
+ int padding = rule.indexOf("padding:");
119
+ assertTrue(padding >= 0, "header.site declares no padding: " + rule);
120
+ String gutter = rule.substring(padding, rule.indexOf(';', padding));
121
+
122
+ assertTrue(gutter.contains("var(--s5)"), "the header gutter must be the fixed spacing token: " + gutter);
123
+ assertFalse(gutter.contains("--content-w"),
124
+ "the header gutter must not follow the per-page content width, or the bar shifts between pages: " + gutter);
125
+ }
126
+
104
127
@Transactional
105
128
User persistUser(String name)
106
129
{