main / internal/interface/web/templates/layout.templ
7.6 KB · Templ Raw
1package templates
2
3import "gitgud/internal/domain"
4
5templ Layout(title string, user *domain.User) {
6 <!DOCTYPE html>
7 <html lang="en" class="dark">
8 <head>
9 <meta charset="UTF-8"/>
10 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
11 <title>{ title }</title>
12 <link rel="preconnect" href="https://fonts.googleapis.com"/>
13 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
14 <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
15 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
16 <style type="text/tailwindcss">
17 @theme {
18 --color-canvas: #0a0a0a;
19 --color-panel: #0e0e0e;
20 --color-panel2: #161616;
21 --color-line: #1f1f1f;
22 --color-line2: #2a2a2a;
23 --color-ink: #ededed;
24 --color-muted: #a1a1a1;
25 --color-faint: #6e6e6e;
26 --color-accent: #ededed;
27 --color-accenthi: #ffffff;
28 --color-grass: #ededed;
29 --color-grasshi: #ffffff;
30 --color-grassink: #0a0a0a;
31 --color-open: #3fb950;
32 --color-closed: #f85149;
33 --color-merged: #a371f7;
34 --color-warn: #d29922;
35 --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
36 --font-mono: "JetBrains Mono", ui-monospace, monospace;
37 }
38 @layer base {
39 html { color-scheme: dark; }
40 body { @apply bg-canvas text-ink font-sans antialiased; }
41 body::before {
42 content: "";
43 position: fixed;
44 inset: 0;
45 pointer-events: none;
46 z-index: 0;
47 background: radial-gradient(680px 320px at 50% -120px, rgba(255,255,255,.05), transparent 70%);
48 }
49 a { @apply transition-colors; }
50 code, pre, .mono { @apply font-mono; }
51 ::selection { background: rgba(255,255,255,.18); }
52 ::-webkit-scrollbar { width: 10px; height: 10px; }
53 ::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 9999px; border: 2px solid #0a0a0a; }
54 :focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255,255,255,.45); border-radius: 6px; }
55 main { animation: fade .15s ease-out both; }
56 @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
57 @media (prefers-reduced-motion: reduce) { main { animation: none; } }
58 }
59 @layer components {
60 .btn { @apply inline-flex items-center gap-1.5 rounded-md border border-line2 bg-panel2 px-3 py-1.5 text-sm font-medium text-ink transition hover:border-faint hover:bg-line; }
61 .btn-primary { @apply border-transparent bg-grass font-semibold text-grassink hover:bg-grasshi hover:border-transparent; }
62 .btn-danger { @apply text-closed hover:border-closed hover:bg-closed/10; }
63 .input, .textarea, .select { @apply w-full rounded-md border border-line2 bg-[#111111] px-3 py-2 text-sm text-ink placeholder:text-faint transition focus:border-faint; }
64 .card { @apply rounded-md border border-line bg-panel; }
65 .link { @apply text-ink hover:text-accent; }
66 .muted { @apply text-muted; }
67 .meta { @apply font-mono text-xs text-faint; }
68 .tab { @apply inline-flex items-center gap-2 border-b-2 border-transparent pb-2.5 text-sm text-muted transition hover:text-ink; }
69 .tab-active { @apply border-accent font-semibold text-ink; }
70 .badge { @apply inline-flex items-center gap-1 rounded border border-line2 px-2 py-0.5 text-xs font-medium text-muted; }
71 .badge-open { @apply gap-1.5 rounded-full border-transparent bg-open/16 px-3 py-1 font-medium text-open; }
72 .badge-closed { @apply gap-1.5 rounded-full border-transparent bg-closed/16 px-3 py-1 font-medium text-[#ff8f86]; }
73 .badge-merged { @apply gap-1.5 rounded-full border-transparent bg-merged/16 px-3 py-1 font-medium text-[#c8a6f7]; }
74 .label-bug { @apply inline-flex items-center rounded-full bg-closed/16 px-2.5 py-0.5 text-[10px] font-medium text-[#ff8f86]; }
75 .label-enh { @apply inline-flex items-center rounded-full bg-accent/16 px-2.5 py-0.5 text-[10px] font-medium text-accenthi; }
76 .label-good { @apply inline-flex items-center rounded-full bg-warn/16 px-2.5 py-0.5 text-[10px] font-medium text-warn; }
77 .row { @apply transition hover:bg-panel2; }
78 .diff-add { @apply bg-open/12 text-[#7ee2a0]; }
79 .diff-del { @apply bg-closed/12 text-[#f2b8b3]; }
80 .diff-hunk { @apply bg-panel2 text-faint; }
81 .prose-dark { @apply text-sm leading-relaxed text-muted; }
82 .prose-dark h1 { @apply mt-5 mb-3 text-xl font-semibold text-ink first:mt-0; }
83 .prose-dark h2 { @apply mt-5 mb-2 text-lg font-semibold text-ink first:mt-0; }
84 .prose-dark h3 { @apply mt-4 mb-2 font-semibold text-ink; }
85 .prose-dark p { @apply my-3; }
86 .prose-dark a { @apply text-accent hover:underline; }
87 .prose-dark strong { @apply font-semibold text-ink; }
88 .prose-dark ul { @apply my-3 list-disc pl-5; }
89 .prose-dark ol { @apply my-3 list-decimal pl-5; }
90 .prose-dark li { @apply my-1; }
91 .prose-dark code { @apply rounded border border-line2 bg-panel2 px-1.5 py-0.5 text-[.85em] text-accent; }
92 .prose-dark pre { @apply my-3 overflow-x-auto rounded-lg border border-line bg-panel2 p-4 text-xs; }
93 .prose-dark pre code { @apply bg-transparent p-0; }
94 .prose-dark blockquote { @apply my-3 border-l-2 border-line2 pl-4 text-faint; }
95 .prose-dark hr { @apply my-5 border-line; }
96 }
97 </style>
98 </head>
99 <body>
100 <header class="sticky top-0 z-20 border-b border-line bg-canvas/80 backdrop-blur-xl">
101 <nav class="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
102 <a href="/" class="flex items-center gap-2.5">
103 <span class="text-accent">@gitLogo("size-5")</span>
104 <span class="font-mono font-semibold tracking-tight text-ink">gitgud</span>
105 </a>
106 <div class="flex items-center gap-4 text-sm">
107 if user != nil {
108 <a class="btn" href="/new">+ New</a>
109 <div class="flex items-center gap-2">
110 @avatar(user.Username, "size-6")
111 <a class="link mono text-muted" href={ templ.SafeURL("/" + user.Username) }>{ user.Username }</a>
112 </div>
113 <form method="post" action="/logout">
114 @csrfField()
115 <button class="text-muted transition hover:text-ink" type="submit">Sign out</button>
116 </form>
117 } else {
118 <a class="text-muted transition hover:text-ink" href="/login">Sign in</a>
119 <a class="btn btn-primary" href="/register">Register</a>
120 }
121 </div>
122 </nav>
123 </header>
124 if flashOf(ctx) != "" {
125 <div class="mx-auto max-w-5xl px-6 pt-4">
126 <div class="card flex items-center gap-2 border-accent/30 bg-accent/10 px-4 py-2.5 text-sm text-ink">
127 <span class="size-1.5 rounded-full bg-accent"></span>
128 { flashOf(ctx) }
129 </div>
130 </div>
131 }
132 <main class="relative z-10 mx-auto max-w-5xl px-6 py-8">
133 { children... }
134 </main>
135 </body>
136 </html>
137}
138
139templ csrfField() {
140 <input type="hidden" name="csrf_token" value={ csrfToken(ctx) }/>
141}
142
143// gitLogo renders the gitgud branch-network mark. Color is inherited via currentColor.
144templ gitLogo(class string) {
145 <svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
146 <circle cx="6" cy="5.5" r="2.5"></circle>
147 <circle cx="6" cy="18.5" r="2.5"></circle>
148 <circle cx="18" cy="12" r="2.5"></circle>
149 <path d="M6 8v8"></path>
150 <path d="M6 12h6.5a3 3 0 0 0 3-3V9.5"></path>
151 </svg>
152}
153
154// avatar renders a deterministic gradient avatar with the user's initial.
155templ avatar(name, class string) {
156 <span
157 class={ "inline-flex shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#2a2a2a] to-[#0e0e0e] font-semibold text-ink ring-1 ring-line2", class }
158 style="font-size:.62em"
159 >{ initial(name) }</span>
160}