π§ Add ui base for login page
Changes
13 files changed, +862 -23
ADD
openspec/changes/ui-design-system/.openspec.yaml
+2 -0
@@ -0,0 +1,2 @@
1
+schema: spec-driven
2
+created: 2026-06-12
ADD
openspec/changes/ui-design-system/design.md
+104 -0
@@ -0,0 +1,104 @@
1
+## Context
2
+
3
+The web UI is server-rendered with Qute (11 templates, one base `layout.html`). All styling currently lives in a
4
+single inline `<style>` block in the layout: dark grey header, system font, default tables. There is no static
5
+resource pipeline yet β `src/main/resources/META-INF/resources/` does not exist. The service compiles to a GraalVM
6
+native image, so everything must be self-contained (no CDN, no build-time CSS tooling that complicates the Maven
7
+build).
8
+
9
+User direction: blue & white (shark colors), light theme only, "modern & nerdy" (terminal font accents) but genuinely
10
+usable with buttons & tabs, optionally hotkeys.
11
+
12
+## Goals / Non-Goals
13
+
14
+**Goals:**
15
+
16
+- One shared stylesheet with CSS custom properties as the single source of design truth
17
+- Shark identity: blue/white light theme, monospace accents, branded header
18
+- Consistent components: buttons, tabs, tables, forms, code blocks, badges
19
+- Tab navigation on repository pages (Files / Commits / Branches)
20
+- Optional hotkeys as progressive enhancement; zero-JS baseline stays fully functional
21
+
22
+**Non-Goals:**
23
+
24
+- Dark mode / theme switching
25
+- CSS frameworks, preprocessors, or a frontend build step (no Tailwind, no npm)
26
+- Client-side rendering or SPA behavior of any kind
27
+- Syntax highlighting for file contents (separate future change)
28
+- Redesigning page structure beyond styling + repo tabs
29
+
30
+## Decisions
31
+
32
+### D1: Hand-written vanilla CSS, no framework
33
+
34
+A single `shark.css` (~300β500 lines) with custom properties on `:root`. Alternatives: Tailwind (needs npm build
35
+step, conflicts with the Maven-only/native-image-simple setup), classless frameworks like Pico.css (fights the
36
+custom shark identity, adds a dependency for little gain). The UI surface is 11 templates β small enough that
37
+hand-written CSS is cheaper than any framework integration.
38
+
39
+### D2: Color tokens
40
+
41
+Shark palette (light theme), defined once as custom properties:
42
+
43
+| Token | Value | Use |
44
+|---|---|---|
45
+| `--shark-blue` | `#1565c0` | primary actions, links, active tab |
46
+| `--shark-deep` | `#0d3b66` | header bar, headings |
47
+| `--shark-fin` | `#4f9fe0` | hover/focus accents |
48
+| `--shark-belly` | `#f4f8fb` | page/code-block tinted background |
49
+| `--surface` | `#ffffff` | cards, tables, main background |
50
+| `--ink` | `#16242f` | body text (dark blue-grey, not pure black) |
51
+| `--ink-muted` | `#5b7282` | secondary text |
52
+| `--border` | `#d3e0ea` | borders, dividers |
53
+| `--danger` | `#c62828` | destructive actions |
54
+
55
+All pairs chosen to clear WCAG AA 4.5:1 against their designated backgrounds (verify during implementation).
56
+
57
+### D3: Typography β system font stacks, no font files
58
+
59
+Body: `system-ui, -apple-system, "Segoe UI", sans-serif`. Terminal accents: `ui-monospace, "SF Mono", "JetBrains
60
+Mono", "Cascadia Code", Menlo, Consolas, monospace`. Alternative considered: self-hosting JetBrains Mono as WOFF2
61
+(~100 KB, needs `@font-face` plus resource-inclusion config for native image). System stacks give the terminal feel
62
+on every modern OS with zero payload and zero native-image config; revisit only if the rendered result disappoints.
63
+This satisfies "self-hosted, no CDN" trivially β there are no font requests at all.
64
+
65
+### D4: Tabs are links, active state from a template parameter
66
+
67
+The repo templates already render per-section pages; each repository page passes an `activeTab` string into the
68
+layout/partial, and the tab markup sets the `active` class by comparison in Qute. No JS, no URL sniffing in CSS.
69
+Resource classes pass at most one extra template parameter β the only Java change in this change.
70
+
71
+### D5: Hotkeys β one small vanilla JS file, allowlist-based
72
+
73
+`shark-hotkeys.js` (<100 lines, self-hosted, `defer`): a keyβaction map (`?` help overlay, `Escape` close, `g h`
74
+home as a two-key sequence with a short timeout). Guard clause ignores events when `event.target` is
75
+input/textarea/select or `isContentEditable`. The help overlay is a `<dialog>` element baked into `layout.html`
76
+(renders nothing visible without JS). Alternatives: a hotkey library (dependency for 5 shortcuts β no) or no JS at
77
+all (drops a feature the user explicitly wants as optional).
78
+
79
+### D6: Static resources via Quarkus default mechanism
80
+
81
+Files go in `src/main/resources/META-INF/resources/` (`shark.css`, `shark-hotkeys.js`), served by Quarkus
82
+automatically, included in native image automatically (classpath resources under META-INF/resources are picked up
83
+by the default resource config). No additional configuration expected.
84
+
85
+## Risks / Trade-offs
86
+
87
+- [Hand-written CSS drifts as templates grow] β tokens-only rule: components must reference custom properties;
88
+ review new templates against the design system spec
89
+- [System monospace stack varies across OS β "terminal feel" weaker on some platforms] β accepted for now; D3
90
+ documents the WOFF2 upgrade path if needed
91
+- [Two-key sequence hotkeys (`g h`) can swallow keystrokes if the timeout logic is buggy] β keep sequence window
92
+ short (~1s), only `g` starts a sequence, everything else is single-key
93
+- [Contrast assumptions in D2 might fail AA for small/muted text] β check pairs with a contrast tool during
94
+ implementation and adjust token values, not component CSS
95
+
96
+## Migration Plan
97
+
98
+Pure additive UI change: ship stylesheet + restyled templates in one release; no data, schema, or API impact.
99
+Rollback = revert the commit.
100
+
101
+## Open Questions
102
+
103
+- None blocking. Shark logo mark: keep the π¦ emoji initially (already in use); a proper SVG mark can replace it
104
+ later without spec changes.
ADD
openspec/changes/ui-design-system/proposal.md
+41 -0
@@ -0,0 +1,41 @@
1
+## Why
2
+
3
+The current web UI is a bare-bones inline-styled layout (dark GitHub-grey header, system font, plain tables) with no
4
+visual identity and no consistent components. git-shark should look like *git-shark*: a blue-and-white shark-themed
5
+light theme with a modern, nerdy terminal aesthetic β while staying genuinely usable through proper buttons, tab
6
+navigation, and optional keyboard shortcuts.
7
+
8
+## What Changes
9
+
10
+- Introduce a design system for the server-rendered UI: design tokens (shark blue/white palette, spacing, typography)
11
+ in a single shared stylesheet served as a static resource, replacing the inline `<style>` block in `layout.html`
12
+- Light theme as the one and only theme (no dark mode in scope)
13
+- Typography: monospace/terminal font for code, refs, hashes, clone URLs, and accents; a clean readable font for body
14
+ text β "nerdy but usable"
15
+- Styled, consistent UI components: buttons (primary/secondary/danger), tab navigation, tables, forms, code blocks,
16
+ badges (visibility, default branch)
17
+- Repository pages get tab navigation (Files / Commits / Branches) instead of bare links
18
+- Optional keyboard shortcuts (hotkeys) for common navigation, progressive enhancement only β UI fully works without
19
+ JavaScript
20
+- All 11 existing Qute templates restyled to use the design system
21
+
22
+## Capabilities
23
+
24
+### New Capabilities
25
+
26
+- `ui-design-system`: Design tokens, shared stylesheet, themed components (buttons, tabs, tables, forms, code blocks,
27
+ badges), shark branding, terminal-accent typography, and optional keyboard shortcuts as progressive enhancement
28
+
29
+### Modified Capabilities
30
+
31
+- `web-ui`: Repository pages SHALL present Files / Commits / Branches as tab navigation; pages SHALL render usable
32
+ without JavaScript (hotkeys are enhancement-only)
33
+
34
+## Impact
35
+
36
+- `src/main/resources/templates/**` β all templates touch the new classes/components; `layout.html` loses its inline
37
+ styles and links the shared stylesheet
38
+- New static resources: `src/main/resources/META-INF/resources/` (stylesheet, optional small hotkey script, font files
39
+ if self-hosted β no external CDN to keep native image self-contained and offline-friendly)
40
+- No backend/Java changes expected beyond possibly passing an "active tab" hint to templates
41
+- No new Maven dependencies; native-build unaffected (static resources only)
ADD
openspec/changes/ui-design-system/specs/ui-design-system/spec.md
+92 -0
@@ -0,0 +1,92 @@
1
+## ADDED Requirements
2
+
3
+### Requirement: Design tokens and shared stylesheet
4
+The system SHALL serve a single shared stylesheet as a static resource that defines all design tokens (colors, spacing, typography, radii) as CSS custom properties. Templates SHALL NOT contain inline `<style>` blocks; all pages SHALL link the shared stylesheet from the base layout.
5
+
6
+#### Scenario: Stylesheet served and linked
7
+- **WHEN** any UI page is rendered
8
+- **THEN** it links the shared stylesheet via `<link rel="stylesheet">` and contains no inline `<style>` block
9
+
10
+#### Scenario: Tokens defined as custom properties
11
+- **WHEN** the shared stylesheet is loaded
12
+- **THEN** colors, spacing, and font families are defined as CSS custom properties on `:root` and component rules reference these tokens
13
+
14
+### Requirement: Shark color palette and light theme
15
+The UI SHALL use a light theme built on a blue-and-white shark palette: white/near-white page backgrounds, shark-blue as the primary accent (header, links, primary buttons, active tab), and dark blue-grey body text. Color contrast for text on its background SHALL meet WCAG AA (β₯ 4.5:1 for normal text).
16
+
17
+#### Scenario: Light theme rendering
18
+- **WHEN** a user opens any page
19
+- **THEN** the page background is white/near-white with blue accents and no dark-mode styling is applied
20
+
21
+#### Scenario: Accessible contrast
22
+- **WHEN** text is rendered on its designated background token
23
+- **THEN** the contrast ratio is at least 4.5:1
24
+
25
+### Requirement: Terminal-accent typography
26
+The UI SHALL render code, file contents, commit hashes, ref names, clone URLs, and SSH key fingerprints in a monospace terminal font stack, and body text/navigation in a clean readable sans-serif stack. Any custom font SHALL be self-hosted; no external CDN requests.
27
+
28
+#### Scenario: Monospace for technical content
29
+- **WHEN** a page shows a commit hash, branch name, clone URL, or file content
30
+- **THEN** it is rendered in the monospace font stack
31
+
32
+#### Scenario: No external font requests
33
+- **WHEN** any page loads
34
+- **THEN** all font resources are served from the application origin
35
+
36
+### Requirement: Button components
37
+The shared stylesheet SHALL provide button styles with at least three variants: primary (shark blue), secondary (outlined/neutral), and danger (destructive actions such as repository or token deletion). All form submit controls and link-buttons in the UI SHALL use one of these variants.
38
+
39
+#### Scenario: Variants available and applied
40
+- **WHEN** a page renders an action control
41
+- **THEN** it uses the primary, secondary, or danger button style, with danger reserved for destructive actions
42
+
43
+### Requirement: Tab navigation component
44
+The shared stylesheet SHALL provide a tab navigation component with a visually distinct active tab. Tabs SHALL be plain links (server-side navigation), not JavaScript-driven.
45
+
46
+#### Scenario: Active tab highlighted
47
+- **WHEN** a page with tab navigation is rendered
48
+- **THEN** the tab corresponding to the current page is visually marked active and the others are plain links
49
+
50
+### Requirement: Styled tables, forms, and code blocks
51
+The shared stylesheet SHALL style tables (repository lists, commit logs, branch/key/token lists), form elements (inputs, textareas, selects, labels), and code/pre blocks consistently with the design tokens.
52
+
53
+#### Scenario: Consistent table styling
54
+- **WHEN** any list page is rendered
55
+- **THEN** its table uses the shared table styles (token-based borders, header row, hover state)
56
+
57
+#### Scenario: Code block styling
58
+- **WHEN** file contents or setup instructions are rendered
59
+- **THEN** they appear in a styled code block with monospace font and tinted background
60
+
61
+### Requirement: Badges
62
+The shared stylesheet SHALL provide badge styles used for repository visibility (public/private) and for marking the default branch.
63
+
64
+#### Scenario: Visibility badge
65
+- **WHEN** a repository is listed or viewed
66
+- **THEN** its visibility is shown as a styled badge
67
+
68
+#### Scenario: Default branch badge
69
+- **WHEN** the branches page is rendered
70
+- **THEN** the default branch carries a badge marking it as default
71
+
72
+### Requirement: Shark branding in header
73
+The base layout SHALL render a branded header: shark mark plus the product name "git-shark" in the accent typography, on a shark-blue header bar, with the primary navigation.
74
+
75
+#### Scenario: Branded header on every page
76
+- **WHEN** any page is rendered
77
+- **THEN** the header shows the shark mark and "git-shark" linking to the home page
78
+
79
+### Requirement: Keyboard shortcuts as progressive enhancement
80
+The UI SHALL offer optional keyboard shortcuts implemented in a single small self-hosted script: at minimum `?` opens a shortcut help overlay, and `g h` navigates home. Shortcuts SHALL NOT fire while focus is in an input, textarea, or select. All functionality reachable via shortcuts SHALL also be reachable by mouse, and every page SHALL remain fully usable with JavaScript disabled.
81
+
82
+#### Scenario: Help overlay
83
+- **WHEN** a user presses `?` outside a form field
84
+- **THEN** an overlay listing all available shortcuts is shown and can be dismissed with `Escape`
85
+
86
+#### Scenario: Shortcuts suppressed in form fields
87
+- **WHEN** focus is inside an input, textarea, or select and a shortcut key is pressed
88
+- **THEN** no shortcut fires and the keystroke goes to the field
89
+
90
+#### Scenario: JavaScript disabled
91
+- **WHEN** a user browses with JavaScript disabled
92
+- **THEN** all pages render and all actions (navigation, forms) work; only shortcuts are unavailable
ADD
openspec/changes/ui-design-system/specs/web-ui/spec.md
+19 -0
@@ -0,0 +1,19 @@
1
+## ADDED Requirements
2
+
3
+### Requirement: Repository tab navigation
4
+Repository pages SHALL present Files, Commits, and Branches as tab navigation using the shared tab component, with the tab of the current page marked active. The tabs SHALL be plain links and preserve the currently selected ref where applicable.
5
+
6
+#### Scenario: Tabs on repository pages
7
+- **WHEN** a user opens a repository's file browser, commit history, or branches page
8
+- **THEN** the page shows Files / Commits / Branches tabs with the current section marked active
9
+
10
+#### Scenario: Ref preserved across tabs
11
+- **WHEN** a user is browsing files at a non-default branch and switches to the Commits tab
12
+- **THEN** the commit history is shown for that same branch
13
+
14
+### Requirement: No-JavaScript usability
15
+All web UI pages SHALL render and remain fully operable (navigation, forms, downloads) without JavaScript. Client-side scripting SHALL only be used for progressive enhancement such as keyboard shortcuts.
16
+
17
+#### Scenario: Full functionality without JavaScript
18
+- **WHEN** a user with JavaScript disabled creates a repository, browses files, and manages SSH keys
19
+- **THEN** every step completes successfully using server-rendered pages and standard form submissions
ADD
openspec/changes/ui-design-system/tasks.md
+37 -0
@@ -0,0 +1,37 @@
1
+## 1. Design tokens & stylesheet
2
+
3
+- [x] 1.1 Create `src/main/resources/META-INF/resources/shark.css` with `:root` design tokens (D2 palette, spacing, radii, font stacks per D3)
4
+- [x] 1.2 Add base styles: page background, body/heading typography, links, `.mono` terminal-accent utility
5
+- [x] 1.3 Add component styles: buttons (`.btn`, `.btn-primary`, `.btn-secondary`, `.btn-danger`), tabs (`.tabs`, `.tab`, `.active`), tables, forms, code/pre blocks, badges (`.badge`, visibility + default-branch variants)
6
+- [x] 1.4 Verify all D2 token pairs meet WCAG AA 4.5:1 contrast; adjust token values if needed (all 12 pairs β₯ 4.71:1)
7
+
8
+## 2. Layout & branding
9
+
10
+- [x] 2.1 Replace the inline `<style>` block in `layout.html` with a `<link rel="stylesheet" href="/shark.css">`
11
+- [x] 2.2 Restyle the header: shark-blue bar (`--shark-deep`), logo mark + "git-shark" wordmark in monospace accent, navigation links (real PNG logo at `/img/shark-logo.png` instead of π¦ emoji)
12
+- [x] 2.3 Add the hidden hotkey help `<dialog>` to `layout.html` (no visible effect without JS)
13
+- [x] 2.4 Write a failing smoke test asserting pages link `/shark.css` and contain no inline `<style>`, then make it green via 2.1 (`DesignSystemTest`)
14
+
15
+## 3. Restyle templates
16
+
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
21
+
22
+## 4. Repository tab navigation
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
27
+
28
+## 5. Hotkeys (progressive enhancement)
29
+
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)
32
+
33
+## 6. Verification
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
37
+- [ ] 6.3 Verify native image still builds and serves `/shark.css` + `/shark-hotkeys.js` (META-INF/resources inclusion per D6)
ADD
src/main/resources/META-INF/resources/img/shark-logo.png
+0 -0
0
0
Binary files differ
ADD
src/main/resources/META-INF/resources/shark-hotkeys.js
+65 -0
@@ -0,0 +1,65 @@
1
+// git-shark hotkeys β progressive enhancement only, the UI works without this.
2
+(function () {
3
+ "use strict";
4
+
5
+ var SEQUENCE_TIMEOUT_MS = 1000;
6
+ var pendingKey = null;
7
+ var pendingTimer = null;
8
+
9
+ function inFormField(target) {
10
+ if (!target) {
11
+ return false;
12
+ }
13
+ var tag = target.tagName;
14
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable;
15
+ }
16
+
17
+ function helpDialog() {
18
+ return document.getElementById("hotkey-help");
19
+ }
20
+
21
+ function clearPending() {
22
+ pendingKey = null;
23
+ if (pendingTimer) {
24
+ clearTimeout(pendingTimer);
25
+ pendingTimer = null;
26
+ }
27
+ }
28
+
29
+ document.addEventListener("keydown", function (event) {
30
+ if (event.ctrlKey || event.metaKey || event.altKey || inFormField(event.target)) {
31
+ return;
32
+ }
33
+
34
+ var dialog = helpDialog();
35
+
36
+ if (event.key === "Escape") {
37
+ if (dialog && dialog.open) {
38
+ dialog.close();
39
+ event.preventDefault();
40
+ }
41
+ clearPending();
42
+ return;
43
+ }
44
+
45
+ if (pendingKey === "g") {
46
+ clearPending();
47
+ if (event.key === "h") {
48
+ event.preventDefault();
49
+ window.location.href = "/";
50
+ }
51
+ return;
52
+ }
53
+
54
+ if (event.key === "g") {
55
+ pendingKey = "g";
56
+ pendingTimer = setTimeout(clearPending, SEQUENCE_TIMEOUT_MS);
57
+ return;
58
+ }
59
+
60
+ if (event.key === "?" && dialog) {
61
+ event.preventDefault();
62
+ dialog.showModal();
63
+ }
64
+ });
65
+})();
ADD
src/main/resources/META-INF/resources/shark.css
+373 -0
@@ -0,0 +1,373 @@
1
+/* git-shark design system β light theme, shark blue & white */
2
+
3
+:root {
4
+ /* palette */
5
+ --shark-blue: #1565c0;
6
+ --shark-deep: #0d3b66;
7
+ --shark-fin: #4f9fe0;
8
+ --shark-belly: #f4f8fb;
9
+ --surface: #ffffff;
10
+ --ink: #16242f;
11
+ --ink-muted: #5b7282;
12
+ --border: #d3e0ea;
13
+ --danger: #c62828;
14
+
15
+ /* typography */
16
+ --font-body: system-ui, -apple-system, "Segoe UI", sans-serif;
17
+ --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
18
+
19
+ /* shape & rhythm */
20
+ --radius: 6px;
21
+ --space-1: 0.25rem;
22
+ --space-2: 0.5rem;
23
+ --space-3: 1rem;
24
+ --space-4: 1.5rem;
25
+ --space-5: 2.5rem;
26
+}
27
+
28
+/* base */
29
+
30
+* {
31
+ box-sizing: border-box;
32
+}
33
+
34
+body {
35
+ margin: 0;
36
+ background: var(--shark-belly);
37
+ color: var(--ink);
38
+ font-family: var(--font-body);
39
+ line-height: 1.5;
40
+}
41
+
42
+h1, h2, h3 {
43
+ color: var(--shark-deep);
44
+ line-height: 1.2;
45
+}
46
+
47
+h1 {
48
+ font-size: 1.5rem;
49
+}
50
+
51
+a {
52
+ color: var(--shark-blue);
53
+ text-decoration: none;
54
+}
55
+
56
+a:hover {
57
+ text-decoration: underline;
58
+}
59
+
60
+.mono {
61
+ font-family: var(--font-mono);
62
+}
63
+
64
+.muted {
65
+ color: var(--ink-muted);
66
+}
67
+
68
+.error {
69
+ color: var(--danger);
70
+}
71
+
72
+/* header */
73
+
74
+header.site {
75
+ display: flex;
76
+ gap: var(--space-4);
77
+ align-items: center;
78
+ padding: var(--space-2) var(--space-4);
79
+ background: var(--shark-deep);
80
+}
81
+
82
+header.site .brand {
83
+ display: flex;
84
+ gap: var(--space-2);
85
+ align-items: center;
86
+ color: #fff;
87
+ font-family: var(--font-mono);
88
+ font-weight: 700;
89
+ font-size: 1.05rem;
90
+}
91
+
92
+header.site .brand:hover {
93
+ text-decoration: none;
94
+ color: var(--shark-fin);
95
+}
96
+
97
+header.site .brand img {
98
+ width: 28px;
99
+ height: 28px;
100
+ border-radius: 50%;
101
+ border: 1px solid var(--shark-fin);
102
+}
103
+
104
+header.site nav {
105
+ display: flex;
106
+ gap: var(--space-3);
107
+ margin-left: auto;
108
+}
109
+
110
+header.site nav a {
111
+ color: #d9e8f5;
112
+ font-size: 0.9rem;
113
+}
114
+
115
+header.site nav a:hover {
116
+ color: #fff;
117
+}
118
+
119
+main {
120
+ max-width: 960px;
121
+ margin: var(--space-5) auto;
122
+ padding: 0 var(--space-4);
123
+}
124
+
125
+/* hero (landing page) */
126
+
127
+.hero {
128
+ display: flex;
129
+ gap: var(--space-4);
130
+ align-items: center;
131
+ margin-bottom: var(--space-5);
132
+}
133
+
134
+.hero img {
135
+ width: 96px;
136
+ height: 96px;
137
+ border-radius: 50%;
138
+ border: 2px solid var(--border);
139
+}
140
+
141
+.hero h1 {
142
+ font-family: var(--font-mono);
143
+ margin: 0 0 var(--space-1);
144
+}
145
+
146
+.hero .tagline {
147
+ font-family: var(--font-mono);
148
+ color: var(--ink-muted);
149
+ margin: 0;
150
+}
151
+
152
+.hero .tagline::before {
153
+ content: "$ ";
154
+ color: var(--shark-fin);
155
+}
156
+
157
+/* buttons */
158
+
159
+.btn {
160
+ display: inline-block;
161
+ padding: var(--space-1) var(--space-3);
162
+ border: 1px solid transparent;
163
+ border-radius: var(--radius);
164
+ font: inherit;
165
+ font-size: 0.9rem;
166
+ cursor: pointer;
167
+ text-decoration: none;
168
+}
169
+
170
+.btn:hover {
171
+ text-decoration: none;
172
+}
173
+
174
+.btn-primary {
175
+ background: var(--shark-blue);
176
+ color: #fff;
177
+}
178
+
179
+.btn-primary:hover {
180
+ background: var(--shark-deep);
181
+}
182
+
183
+.btn-secondary {
184
+ background: var(--surface);
185
+ color: var(--shark-blue);
186
+ border-color: var(--border);
187
+}
188
+
189
+.btn-secondary:hover {
190
+ border-color: var(--shark-blue);
191
+}
192
+
193
+.btn-danger {
194
+ background: var(--surface);
195
+ color: var(--danger);
196
+ border-color: var(--border);
197
+}
198
+
199
+.btn-danger:hover {
200
+ background: var(--danger);
201
+ border-color: var(--danger);
202
+ color: #fff;
203
+}
204
+
205
+/* tabs */
206
+
207
+.tabs {
208
+ display: flex;
209
+ gap: var(--space-2);
210
+ border-bottom: 2px solid var(--border);
211
+ margin-bottom: var(--space-3);
212
+}
213
+
214
+.tab {
215
+ padding: var(--space-1) var(--space-3);
216
+ color: var(--ink-muted);
217
+ border-bottom: 2px solid transparent;
218
+ margin-bottom: -2px;
219
+ font-family: var(--font-mono);
220
+ font-size: 0.9rem;
221
+}
222
+
223
+.tab:hover {
224
+ color: var(--shark-blue);
225
+ text-decoration: none;
226
+}
227
+
228
+.tab.active {
229
+ color: var(--shark-deep);
230
+ border-bottom-color: var(--shark-blue);
231
+ font-weight: 600;
232
+}
233
+
234
+/* tables */
235
+
236
+table {
237
+ border-collapse: collapse;
238
+ width: 100%;
239
+ background: var(--surface);
240
+ border: 1px solid var(--border);
241
+ border-radius: var(--radius);
242
+}
243
+
244
+td, th {
245
+ border-bottom: 1px solid var(--border);
246
+ padding: var(--space-2) var(--space-3);
247
+ text-align: left;
248
+}
249
+
250
+th {
251
+ background: var(--shark-belly);
252
+ color: var(--shark-deep);
253
+ font-family: var(--font-mono);
254
+ font-size: 0.85rem;
255
+}
256
+
257
+tr:last-child td {
258
+ border-bottom: none;
259
+}
260
+
261
+tbody tr:hover, table tr:hover td {
262
+ background: var(--shark-belly);
263
+}
264
+
265
+/* forms */
266
+
267
+label {
268
+ display: block;
269
+ color: var(--shark-deep);
270
+ font-size: 0.9rem;
271
+ font-weight: 600;
272
+}
273
+
274
+input, textarea, select {
275
+ font: inherit;
276
+ color: var(--ink);
277
+ background: var(--surface);
278
+ border: 1px solid var(--border);
279
+ border-radius: var(--radius);
280
+ padding: var(--space-1) var(--space-2);
281
+ margin-top: var(--space-1);
282
+}
283
+
284
+input:focus, textarea:focus, select:focus {
285
+ outline: 2px solid var(--shark-fin);
286
+ outline-offset: 0;
287
+ border-color: var(--shark-blue);
288
+}
289
+
290
+textarea {
291
+ width: 100%;
292
+ min-height: 6rem;
293
+ font-family: var(--font-mono);
294
+ font-size: 0.85rem;
295
+}
296
+
297
+form.inline {
298
+ display: inline;
299
+}
300
+
301
+/* code */
302
+
303
+code, pre {
304
+ font-family: var(--font-mono);
305
+ font-size: 0.85rem;
306
+ background: var(--shark-belly);
307
+ border-radius: 4px;
308
+}
309
+
310
+code {
311
+ padding: 0.1rem 0.3rem;
312
+}
313
+
314
+pre {
315
+ border: 1px solid var(--border);
316
+ padding: var(--space-3);
317
+ overflow-x: auto;
318
+}
319
+
320
+pre code {
321
+ padding: 0;
322
+ background: none;
323
+}
324
+
325
+/* badges */
326
+
327
+.badge {
328
+ display: inline-block;
329
+ padding: 0 var(--space-2);
330
+ border: 1px solid var(--border);
331
+ border-radius: 999px;
332
+ font-family: var(--font-mono);
333
+ font-size: 0.75rem;
334
+ color: var(--ink-muted);
335
+}
336
+
337
+.badge-public {
338
+ color: var(--shark-blue);
339
+ border-color: var(--shark-fin);
340
+}
341
+
342
+.badge-private {
343
+ color: var(--shark-deep);
344
+ background: var(--shark-belly);
345
+}
346
+
347
+.badge-default {
348
+ color: #fff;
349
+ background: var(--shark-blue);
350
+ border-color: var(--shark-blue);
351
+}
352
+
353
+/* hotkey help dialog */
354
+
355
+dialog#hotkey-help {
356
+ border: 1px solid var(--border);
357
+ border-radius: var(--radius);
358
+ padding: var(--space-4);
359
+ min-width: 20rem;
360
+}
361
+
362
+dialog#hotkey-help::backdrop {
363
+ background: rgba(13, 59, 102, 0.4);
364
+}
365
+
366
+dialog#hotkey-help kbd {
367
+ font-family: var(--font-mono);
368
+ font-size: 0.8rem;
369
+ background: var(--shark-belly);
370
+ border: 1px solid var(--border);
371
+ border-radius: 4px;
372
+ padding: 0 var(--space-1);
373
+}
MODIFY
src/main/resources/templates/HomeResource/home.html
+12 -5
@@ -1,16 +1,23 @@
1
1
{#include layout}
2
2
{#title}git-shark{/title}
3
-<h1>Repositories</h1>
3
+<section class="hero">
4
+ <img src="/img/shark-logo.png" alt="git-shark logo">
5
+ <div>
6
+ <h1>git-shark</h1>
7
+ <p class="tagline">self-hosted git hosting β natively compiled, bite-sized</p>
8
+ </div>
9
+</section>
10
+<h2>Repositories</h2>
4
11
{#if user}
5
-<p><a href="/repos/new">New repository</a></p>
12
+<p><a class="btn btn-primary" href="/repos/new">New repository</a></p>
6
13
{/if}
7
14
<table>
8
15
<tr><th>Repository</th><th>Visibility</th><th>Description</th></tr>
9
16
{#for repo in repositories}
10
17
<tr>
11
- <td><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></td>
12
- <td>{repo.visibility}</td>
13
- <td>{repo.description ?: ''}</td>
18
+ <td><a class="mono" href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></td>
19
+ <td><span class="badge badge-{repo.visibility.name().toLowerCase()}">{repo.visibility.name().toLowerCase()}</span></td>
20
+ <td class="muted">{repo.description ?: ''}</td>
14
21
</tr>
15
22
{/for}
16
23
</table>
MODIFY
src/main/resources/templates/HomeResource/newRepo.html
+2 -2
@@ -5,7 +5,7 @@
5
5
<p class="error">{error}</p>
6
6
{/if}
7
7
<form method="post" action="/repos">
8
- <p><label>Name <input name="name" required pattern="[a-zA-Z0-9._-]+"></label></p>
8
+ <p><label>Name <input class="mono" name="name" required pattern="[a-zA-Z0-9._-]+"></label></p>
9
9
<p><label>Visibility
10
10
<select name="visibility">
11
11
<option value="PUBLIC">Public</option>
@@ -13,6 +13,6 @@
13
13
</select>
14
14
</label></p>
15
15
<p><label>Description <input name="description"></label></p>
16
- <button>Create repository</button>
16
+ <button class="btn btn-primary">Create repository</button>
17
17
</form>
18
18
{/include}
MODIFY
src/main/resources/templates/layout.html
+13 -16
@@ -4,24 +4,13 @@
4
4
<meta charset="utf-8">
5
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
6
<title>{#insert title}git-shark{/}</title>
7
- <style>
8
- body { font-family: system-ui, sans-serif; margin: 0; color: #1f2328; }
9
- header { display: flex; gap: 1.5rem; align-items: center; padding: 0.75rem 1.5rem; background: #24292f; }
10
- header a { color: #fff; text-decoration: none; font-weight: 600; }
11
- header nav { display: flex; gap: 1rem; margin-left: auto; }
12
- header nav a { font-weight: 400; }
13
- main { max-width: 960px; margin: 2rem auto; padding: 0 1.5rem; }
14
- table { border-collapse: collapse; width: 100%; }
15
- td, th { border: 1px solid #d0d7de; padding: 0.4rem 0.6rem; text-align: left; }
16
- code { background: #f6f8fa; padding: 0.1rem 0.3rem; border-radius: 4px; }
17
- .error { color: #cf222e; }
18
- form.inline { display: inline; }
19
- textarea { width: 100%; min-height: 6rem; }
20
- </style>
7
+ <link rel="icon" type="image/png" href="/img/shark-logo.png">
8
+ <link rel="stylesheet" href="/shark.css">
9
+ <script defer src="/shark-hotkeys.js"></script>
21
10
</head>
22
11
<body>
23
-<header>
24
- <a href="/">🦈 git-shark</a>
12
+<header class="site">
13
+ <a class="brand" href="/"><img src="/img/shark-logo.png" alt="git-shark logo">git-shark</a>
25
14
<nav>
26
15
<a href="/settings/keys">SSH keys</a>
27
16
<a href="/settings/tokens">Access tokens</a>
@@ -31,5 +20,13 @@
31
20
<main>
32
21
{#insert /}
33
22
</main>
23
+<dialog id="hotkey-help">
24
+ <h2>Keyboard shortcuts</h2>
25
+ <table>
26
+ <tr><td><kbd>?</kbd></td><td>Show this help</td></tr>
27
+ <tr><td><kbd>g</kbd> <kbd>h</kbd></td><td>Go home</td></tr>
28
+ <tr><td><kbd>Esc</kbd></td><td>Close dialog</td></tr>
29
+ </table>
30
+</dialog>
34
31
</body>
35
32
</html>
ADD
src/test/java/de/workaround/web/DesignSystemTest.java
+102 -0
@@ -0,0 +1,102 @@
1
+package de.workaround.web;
2
+
3
+import java.util.UUID;
4
+
5
+import org.junit.jupiter.api.Test;
6
+
7
+import de.workaround.git.GitRepositoryService;
8
+import de.workaround.model.Repository;
9
+import de.workaround.model.User;
10
+import io.quarkus.test.junit.QuarkusTest;
11
+import jakarta.inject.Inject;
12
+import jakarta.transaction.Transactional;
13
+
14
+import static io.restassured.RestAssured.given;
15
+import static org.hamcrest.CoreMatchers.containsString;
16
+import static org.hamcrest.CoreMatchers.not;
17
+
18
+@QuarkusTest
19
+class DesignSystemTest
20
+{
21
+ @Inject
22
+ GitRepositoryService service;
23
+
24
+ @Test
25
+ void pagesLinkSharedStylesheetAndHaveNoInlineStyles()
26
+ {
27
+ given().when().get("/")
28
+ .then().statusCode(200)
29
+ .body(containsString("<link rel=\"stylesheet\" href=\"/shark.css\">"))
30
+ .body(not(containsString("<style>")));
31
+ }
32
+
33
+ @Test
34
+ void sharedStylesheetDefinesSharkTokens()
35
+ {
36
+ given().when().get("/shark.css")
37
+ .then().statusCode(200)
38
+ .body(containsString(":root"))
39
+ .body(containsString("--shark-blue"))
40
+ .body(containsString("--shark-deep"));
41
+ }
42
+
43
+ @Test
44
+ void landingPageShowsLogoAndBrandedHeader()
45
+ {
46
+ given().when().get("/")
47
+ .then().statusCode(200)
48
+ .body(containsString("/img/shark-logo.png"))
49
+ .body(containsString("git-shark"));
50
+ }
51
+
52
+ @Test
53
+ void logoIsServedAsStaticResource()
54
+ {
55
+ given().when().get("/img/shark-logo.png")
56
+ .then().statusCode(200)
57
+ .header("Content-Type", containsString("image/png"));
58
+ }
59
+
60
+ @Test
61
+ void landingPageShowsVisibilityBadges()
62
+ {
63
+ User owner = persistUser("ds-owner-" + unique());
64
+ service.create(owner, "ds-badged", Repository.Visibility.PUBLIC, "demo");
65
+
66
+ given().when().get("/")
67
+ .then().statusCode(200)
68
+ .body(containsString("class=\"badge"));
69
+ }
70
+
71
+ @Test
72
+ void layoutContainsHotkeyHelpDialog()
73
+ {
74
+ given().when().get("/")
75
+ .then().statusCode(200)
76
+ .body(containsString("<dialog id=\"hotkey-help\""))
77
+ .body(containsString("/shark-hotkeys.js"));
78
+ }
79
+
80
+ private static String unique()
81
+ {
82
+ return UUID.randomUUID().toString().substring(0, 8);
83
+ }
84
+
85
+ @Transactional
86
+ User persistUser(String name)
87
+ {
88
+ User existing = userRepo.findByOidcSubOptional(name).orElse(null);
89
+ if (existing != null)
90
+ {
91
+ return existing;
92
+ }
93
+ User user = new User();
94
+ user.oidcSub = name;
95
+ user.username = name;
96
+ user.persist();
97
+ return user;
98
+ }
99
+
100
+ @Inject
101
+ User.Repo userRepo;
102
+}