rin
Публичный Open-source lightweight git hosting.

feat(repo): inline description that expands on click with a growing avatar

- repo header: description sits on the title line, truncates with an
  ellipsis and expands onto its own line on click; the avatar animates
  to match the height of the name + description block
- release form: styled drag-and-drop chooser for release assets
- /agents: English guide for AI agents (device flow, git ops, scripted
  UI, releases with assets) with sidebar and command-palette links
- keep assets/input.css in sync with the served app.css
R rin · 2026-10-16 18:11 UTC
11 файлов изменено +355 -17
assets/input.css
+48 -1
... ... @@ -373,6 +373,13 @@ html[data-sidebar="collapsed"] .sidebar-inner {
373 373 .nav-item:active {
374 374 transform: scale(0.985);
375 375 }
376 +.muted-link {
377 + color: hsl(var(--muted-foreground));
378 + transition: color 140ms ease;
379 +}
380 +.muted-link:hover {
381 + color: hsl(var(--foreground));
382 +}
376 383 .nav-section-label {
377 384 display: flex;
378 385 align-items: center;
... ... @@ -734,6 +741,7 @@ kbd, .kbd {
734 741 }
735 742 }
736 743
744 +
737 745 /* Separator */
738 746 .separator {
739 747 height: 1px;
... ... @@ -1273,7 +1281,7 @@ kbd, .kbd {
1273 1281 justify-content: space-between;
1274 1282 gap: 1rem;
1275 1283 flex-wrap: wrap;
1276 - padding: 0.9rem 1rem 0.95rem;
1284 + padding: 0.65rem 1rem;
1277 1285 background: hsl(var(--card));
1278 1286 border: 1px solid hsl(var(--border));
1279 1287 border-radius: calc(var(--radius) * 1.6);
... ... @@ -1293,6 +1301,45 @@ kbd, .kbd {
1293 1301 gap: 0.4rem;
1294 1302 flex-wrap: wrap;
1295 1303 }
1304 +.repo-head-block {
1305 + min-width: 0;
1306 + flex: 1 1 auto;
1307 +}
1308 +.repo-head-row {
1309 + display: flex;
1310 + align-items: center;
1311 + gap: 0.55rem;
1312 + flex-wrap: wrap;
1313 + min-width: 0;
1314 +}
1315 +.repo-desc {
1316 + flex: 1 1 6rem;
1317 + min-width: 0;
1318 + max-width: 36rem;
1319 + overflow: hidden;
1320 + text-overflow: ellipsis;
1321 + white-space: nowrap;
1322 + font-size: 0.8rem;
1323 + line-height: 1.35;
1324 + color: hsl(var(--muted-foreground));
1325 + transition: color 0.15s ease;
1326 +}
1327 +.repo-desc.truncated {
1328 + cursor: pointer;
1329 +}
1330 +.repo-desc.truncated:hover {
1331 + color: hsl(var(--foreground));
1332 +}
1333 +.repo-banner.desc-open .repo-desc {
1334 + flex-basis: 100%;
1335 + max-width: 100%;
1336 + white-space: normal;
1337 + overflow: visible;
1338 + overflow-wrap: anywhere;
1339 +}
1340 +.repo-avatar {
1341 + transition: width 0.2s ease, height 0.2s ease, font-size 0.2s ease;
1342 +}
1296 1343
1297 1344
1298 1345
internal/i18n/i18n.go
+2 -0
... ... @@ -25,6 +25,7 @@ var translations = map[Lang]map[string]string{
25 25 "nav.explore": "Обзор",
26 26 "nav.settings": "Настройки",
27 27 "nav.admin": "Админка",
28 + "nav.agents": "Для AI-агентов",
28 29 "nav.login": "Войти",
29 30 "nav.register": "Регистрация",
30 31 "nav.logout": "Выйти",
... ... @@ -531,6 +532,7 @@ var translations = map[Lang]map[string]string{
531 532 "nav.explore": "Explore",
532 533 "nav.settings": "Settings",
533 534 "nav.admin": "Admin",
535 + "nav.agents": "For AI agents",
534 536 "nav.login": "Sign in",
535 537 "nav.register": "Sign up",
536 538 "nav.logout": "Sign out",
internal/web/handlers/agents.go
+13 -0
... ... @@ -0,0 +1,13 @@
1 +package handlers
2 +
3 +import (
4 + "github.com/gofiber/fiber/v2"
5 +)
6 +
7 +// AgentsPage renders the public guide for AI agents working with this instance.
8 +// GET /agents
9 +func (h *Handler) AgentsPage(c *fiber.Ctx) error {
10 + data := h.baseData(c, "AI agents · Polar")
11 + data["SiteURL"] = h.siteURL(c)
12 + return h.render(c, fiber.StatusOK, "agents", data)
13 +}
internal/web/handlers/agents_test.go
+30 -0
... ... @@ -0,0 +1,30 @@
1 +package handlers
2 +
3 +import (
4 + "net/http"
5 + "strings"
6 + "testing"
7 +)
8 +
9 +// TestAgentsPage checks that the public agent guide renders for anonymous visitors.
10 +func TestAgentsPage(t *testing.T) {
11 + app, _, _, _ := setupTestApp(t)
12 +
13 + status, body := getPage(t, app, "/agents")
14 + if status != http.StatusOK {
15 + t.Fatalf("GET /agents status %d", status)
16 + }
17 + for _, want := range []string{
18 + "Polar for AI agents",
19 + "RFC 8628",
20 + "/api/v1/device/code",
21 + "/api/v1/device/token",
22 + "polar_session",
23 + "releases/new",
24 + "git-credential-polar",
25 + } {
26 + if !strings.Contains(body, want) {
27 + t.Errorf("/agents page body missing %q", want)
28 + }
29 + }
30 +}
internal/web/handlers/handlers.go
+1 -0
... ... @@ -74,6 +74,7 @@ func (h *Handler) RegisterRoutes(app *fiber.App) {
74 74 // Public & Root routes
75 75 app.Get("/", h.Home)
76 76 app.Get("/explore", h.Explore)
77 + app.Get("/agents", h.AgentsPage)
77 78
78 79 // Auth routes
79 80 app.Get("/login", h.LoginPage)
internal/web/static/css/app.css
+47 -1
... ... @@ -373,6 +373,13 @@ html[data-sidebar="collapsed"] .sidebar-inner {
373 373 .nav-item:active {
374 374 transform: scale(0.985);
375 375 }
376 +.muted-link {
377 + color: hsl(var(--muted-foreground));
378 + transition: color 140ms ease;
379 +}
380 +.muted-link:hover {
381 + color: hsl(var(--foreground));
382 +}
376 383 .nav-section-label {
377 384 display: flex;
378 385 align-items: center;
... ... @@ -1274,7 +1281,7 @@ kbd, .kbd {
1274 1281 justify-content: space-between;
1275 1282 gap: 1rem;
1276 1283 flex-wrap: wrap;
1277 - padding: 0.9rem 1rem 0.95rem;
1284 + padding: 0.65rem 1rem;
1278 1285 background: hsl(var(--card));
1279 1286 border: 1px solid hsl(var(--border));
1280 1287 border-radius: calc(var(--radius) * 1.6);
... ... @@ -1294,6 +1301,45 @@ kbd, .kbd {
1294 1301 gap: 0.4rem;
1295 1302 flex-wrap: wrap;
1296 1303 }
1304 +.repo-head-block {
1305 + min-width: 0;
1306 + flex: 1 1 auto;
1307 +}
1308 +.repo-head-row {
1309 + display: flex;
1310 + align-items: center;
1311 + gap: 0.55rem;
1312 + flex-wrap: wrap;
1313 + min-width: 0;
1314 +}
1315 +.repo-desc {
1316 + flex: 1 1 6rem;
1317 + min-width: 0;
1318 + max-width: 36rem;
1319 + overflow: hidden;
1320 + text-overflow: ellipsis;
1321 + white-space: nowrap;
1322 + font-size: 0.8rem;
1323 + line-height: 1.35;
1324 + color: hsl(var(--muted-foreground));
1325 + transition: color 0.15s ease;
1326 +}
1327 +.repo-desc.truncated {
1328 + cursor: pointer;
1329 +}
1330 +.repo-desc.truncated:hover {
1331 + color: hsl(var(--foreground));
1332 +}
1333 +.repo-banner.desc-open .repo-desc {
1334 + flex-basis: 100%;
1335 + max-width: 100%;
1336 + white-space: normal;
1337 + overflow: visible;
1338 + overflow-wrap: anywhere;
1339 +}
1340 +.repo-avatar {
1341 + transition: width 0.2s ease, height 0.2s ease, font-size 0.2s ease;
1342 +}
1297 1343
1298 1344
1299 1345
internal/web/static/js/app.js
+53 -1
... ... @@ -463,5 +463,57 @@
463 463 rmBtn.style.display = 'none';
464 464 }
465 465 });
466 -})();
467 466
467 + // Repo header: the description stays on the title line and expands onto its
468 + // own line when clicked. The avatar grows to match the name + description
469 + // block instead of staying centered.
470 + var repoAvatarTimer = null;
471 +
472 + function sizeRepoAvatars() {
473 + var changed = false;
474 + document.querySelectorAll('.repo-banner').forEach(function (banner) {
475 + var avatar = banner.querySelector('.repo-avatar');
476 + var block = banner.querySelector('.repo-head-block');
477 + if (!avatar || !block) return;
478 + var h = Math.round(block.getBoundingClientRect().height);
479 + if (h <= 0) return;
480 + var size = h + 'px';
481 + if (avatar.style.width === size) return;
482 + avatar.style.width = size;
483 + avatar.style.height = size;
484 + avatar.style.fontSize = Math.min(22, Math.max(9, Math.round(h * 0.4))) + 'px';
485 + changed = true;
486 + });
487 + return changed;
488 + }
489 +
490 + function updateRepoDescs() {
491 + document.querySelectorAll('[data-repo-desc]').forEach(function (el) {
492 + var banner = el.closest('.repo-banner');
493 + if (banner && banner.classList.contains('desc-open')) {
494 + el.classList.add('truncated');
495 + return;
496 + }
497 + el.classList.toggle('truncated', el.scrollWidth > el.clientWidth + 1);
498 + });
499 + if (sizeRepoAvatars()) {
500 + // Re-measure once the width transition settles: the final avatar width
501 + // changes the room left for the description.
502 + window.clearTimeout(repoAvatarTimer);
503 + repoAvatarTimer = window.setTimeout(sizeRepoAvatars, 300);
504 + }
505 + }
506 +
507 + document.addEventListener('click', function (e) {
508 + var desc = e.target.closest('[data-repo-desc]');
509 + if (!desc || !desc.classList.contains('truncated')) return;
510 + var banner = desc.closest('.repo-banner');
511 + if (!banner) return;
512 + banner.classList.toggle('desc-open');
513 + updateRepoDescs();
514 + });
515 +
516 + window.addEventListener('resize', updateRepoDescs);
517 + document.addEventListener('htmx:afterSwap', updateRepoDescs);
518 + updateRepoDescs();
519 +})();
internal/web/views/layouts/base.html
+9 -0
... ... @@ -124,6 +124,12 @@
124 124 {{end}}
125 125 </div>
126 126 {{end}}
127 + <div style="padding:0 0.35rem;">
128 + <a href="/agents" class="muted-link flex items-center gap-1.5 text-xs" style="padding:0.1rem 0;">
129 + {{icon "book-open" "size-3.5"}}
130 + <span>{{t .Lang "nav.agents"}}</span>
131 + </a>
132 + </div>
127 133 </div>
128 134 </div>
129 135 </aside>
... ... @@ -224,6 +230,9 @@
224 230 <span class="flex items-center gap-1"><kbd class="kbd">g</kbd><kbd class="kbd">r</kbd></span>
225 231 </button>
226 232 {{end}}
233 + <button type="button" class="palette-item" data-palette-item data-palette-href="/agents" data-palette-keywords="agents ai api guide агенты документация">
234 + <span class="flex items-center gap-2">{{icon "book-open" "size-4"}} {{t .Lang "nav.agents"}}</span>
235 + </button>
227 236 {{if .CurrentUser}}
228 237 <button type="button" class="palette-item" data-palette-item data-palette-href="/new" data-palette-keywords="new create создать репозиторий">
229 238 <span class="flex items-center gap-2">{{icon "plus" "size-4"}} {{t .Lang "repo.new.title"}}</span>
internal/web/views/pages/agents.html
+106 -0
... ... @@ -0,0 +1,106 @@
1 +{{define "content"}}
2 +<div class="container" style="max-width:880px;">
3 + <div class="flex items-start justify-between flex-wrap gap-2 mb-4">
4 + <div>
5 + <h1 class="text-xl font-bold tracking-tight" style="margin:0;">Polar for AI agents</h1>
6 + <div class="text-xs text-muted mt-1">A practical guide to cloning, pushing, publishing and automating this Git host.</div>
7 + </div>
8 + <span class="badge badge-outline font-mono text-xs">GET /agents</span>
9 + </div>
10 +
11 + <article class="card">
12 + <div class="card-body markdown-body">
13 + <h2>Overview</h2>
14 + <p>Polar is a self-hosted Git forge at <code>{{.SiteURL}}</code>. It serves standard <strong>Git over HTTPS</strong> (smart HTTP) plus a web UI for issues, pull requests, releases and repository settings. Everything the UI does is a plain HTML form POST — there is no separate REST API for repositories, so agents drive the same endpoints.</p>
15 +
16 + <h2>Authentication: device flow</h2>
17 + <p>Git over HTTPS uses the <strong>OAuth 2.0 Device Authorization Grant (RFC 8628)</strong>: the client asks for a short user code, the human approves it in the browser once, and the client receives a personal access token.</p>
18 + <ol>
19 + <li>Request a device code from <code>POST /api/v1/device/code</code> with a <code>client_id</code>.</li>
20 + <li>Send the user to <code>verification_uri_complete</code> (they must be signed in to Polar).</li>
21 + <li>Poll <code>POST /api/v1/device/token</code> with <code>device_code</code> every <code>interval</code> seconds (default 5). While the grant is pending the endpoint answers <code>429 {"error":"authorization_pending"}</code>.</li>
22 + <li>On approval you receive <code>{"access_token":"...","token_type":"bearer","username":"...","scope":"git"}</code>.</li>
23 + </ol>
24 + <pre><code># 1. Start a device grant (no browser session required for this step)
25 +curl -sX POST {{.SiteURL}}/api/v1/device/code -d 'client_id=my-agent'
26 +# {"device_code":"...","user_code":"WDJB-MJHT","verification_uri":"{{.SiteURL}}/login/device",
27 +# "verification_uri_complete":"{{.SiteURL}}/login/device?user_code=WDJB-MJHT","expires_in":600,"interval":5}
28 +
29 +# 2. Ask the user to open verification_uri_complete and approve.
30 +
31 +# 3. Exchange the approved device code for a token
32 +curl -sX POST {{.SiteURL}}/api/v1/device/token -d 'device_code=DEVICE_CODE'
33 +# {"access_token":"...","token_type":"bearer","username":"rin","scope":"git"}</code></pre>
34 + <p>Use the token as the password for HTTPS Git with any username:</p>
35 + <pre><code>git clone https://USERNAME:TOKEN@HOST/OWNER/REPO.git</code></pre>
36 + <p>This is the same handoff a fresh <code>git push</code> triggers: Polar opens the approval page and the credential helper waits for it. For scripts, <code>scripts/git-credential-polar.sh</code> automates the flow and caches tokens in <code>~/.polar/credentials.json</code>:</p>
37 + <pre><code>git config --global credential.helper '!sh /path/to/polar/scripts/git-credential-polar.sh'</code></pre>
38 + <p>Tokens are listed and revocable under <em>Settings → Access tokens</em>. Device codes are single-use and expire after <code>expires_in</code> seconds.</p>
39 +
40 + <h2>Git operations</h2>
41 + <ul>
42 + <li><strong>Clone / fetch / push:</strong> <code>https://HOST/OWNER/REPO.git</code> — standard smart HTTP.</li>
43 + <li><strong>Branches and tags:</strong> push them normally; Polar stores refs and shows them in the UI.</li>
44 + <li><strong>Public repositories:</strong> readable anonymously when anonymous read is enabled on the instance.</li>
45 + <li><strong>Private repositories:</strong> every Git request needs credentials — unauthenticated requests get <code>401</code>, while the web UI answers <code>404</code> so it does not leak existence.</li>
46 + <li><strong>Empty repositories:</strong> the first <code>git push -u origin main</code> creates the default branch.</li>
47 + </ul>
48 +
49 + <h2>Driving the web UI from a script</h2>
50 + <p>The UI is server-rendered HTML with plain forms, which makes it fully scriptable:</p>
51 + <ul>
52 + <li>Sign in by POSTing <code>/login</code> (<code>username</code>, <code>password</code>) and keeping the <code>polar_session</code> cookie.</li>
53 + <li>Every non-GET request needs the session CSRF token: read the hidden <code>_csrf</code> input from any page and send it as the <code>_csrf</code> form field or the <code>X-CSRF-Token</code> header.</li>
54 + <li>File uploads are <code>multipart/form-data</code>.</li>
55 + </ul>
56 + <pre><code># Close an issue (state: open|closed)
57 +curl -b cookies.txt -X POST https://HOST/OWNER/REPO/issues/1/state \
58 + -H "X-CSRF-Token: $CSRF" -d 'state=closed'
59 +
60 +# Comment on an issue
61 +curl -b cookies.txt -X POST https://HOST/OWNER/REPO/issues/1/comments \
62 + -H "X-CSRF-Token: $CSRF" -d 'body=Fixed in the latest build.'
63 +
64 +# Open a pull request (head_branch must already be pushed)
65 +curl -b cookies.txt -X POST https://HOST/OWNER/REPO/pulls/new \
66 + -H "X-CSRF-Token: $CSRF" \
67 + -d 'title=Rework uploads' -d 'body=Release notes...' \
68 + -d 'base_branch=main' -d 'head_branch=feature/uploads'
69 +
70 +# Review and merge a pull request
71 +curl -b cookies.txt -X POST https://HOST/OWNER/REPO/pulls/4/review \
72 + -H "X-CSRF-Token: $CSRF" -d 'state=approved' -d 'body=LGTM'
73 +curl -b cookies.txt -X POST https://HOST/OWNER/REPO/pulls/4/merge \
74 + -H "X-CSRF-Token: $CSRF"</code></pre>
75 + <p>Other form endpoints: <code>POST /OWNER/REPO/issues/new</code> (<code>title</code>, <code>body</code>), <code>POST /OWNER/REPO/upload</code> (<code>files</code>, <code>ref</code>, <code>path</code>, <code>message</code>), <code>POST /OWNER/REPO/settings/collaborators/add</code>, <code>POST /OWNER/REPO/fork</code>.</p>
76 +
77 + <h2>Releases and artifacts</h2>
78 + <p>Releases group a git tag, notes and uploaded files. Publishing one is a single multipart POST:</p>
79 + <pre><code>curl -b cookies.txt -X POST https://HOST/OWNER/REPO/releases/new \
80 + -H "X-CSRF-Token: $CSRF" \
81 + -F 'tag_name=v1.2.0' -F 'target_ref=main' \
82 + -F 'title=v1.2.0' -F 'body=Release notes...' -F 'is_prerelease=true' \
83 + -F 'assets=@dist/app-linux-amd64' -F 'assets=@dist/app-windows-amd64.exe'</code></pre>
84 + <ul>
85 + <li>The tag is created automatically if missing, pointing at <code>target_ref</code>.</li>
86 + <li>Up to 20 assets per release, 100 MB per file. Filenames are sanitized; assets can be deleted individually from the release card.</li>
87 + <li>Asset downloads support HTTP <code>Range</code>: <code>GET /OWNER/REPO/releases/&lt;id&gt;/assets/&lt;assetID&gt;/download</code>.</li>
88 + <li>Tag archives are always available: <code>GET /OWNER/REPO/archive/v1.2.0.zip</code> or <code>.tar.gz</code>.</li>
89 + <li>Raw file contents: <code>GET /OWNER/REPO/raw/&lt;ref&gt;/&lt;path&gt;</code>.</li>
90 + </ul>
91 +
92 + <h2>Working agreements</h2>
93 + <ul>
94 + <li>Prefer branches and pull requests over pushing straight to the default branch in shared repositories.</li>
95 + <li>Match the repository's commit message style; explain the change, not the process.</li>
96 + <li>Use tags for release points and attach artifacts with stable names (for example <code>polar-linux-amd64</code>).</li>
97 + <li>Treat tokens as secrets: never commit them, and revoke what you no longer need.</li>
98 + <li>Poll the device token endpoint no faster than the advertised <code>interval</code>; back off on <code>429</code>.</li>
99 + </ul>
100 +
101 + <hr>
102 + <p class="text-xs text-muted">Static documentation. Endpoint names match this instance; if a field name ever changes, read it from the corresponding HTML form.</p>
103 + </div>
104 + </article>
105 +</div>
106 +{{end}}
internal/web/views/pages/release_new.html
+35 -4
... ... @@ -45,10 +45,15 @@
45 45 <textarea id="body" name="body" class="textarea" rows="8" placeholder="{{t .Lang "releases.body_ph"}}" maxlength="50000"></textarea>
46 46 </div>
47 47
48 - <div>
49 - <label class="label" for="assets">{{t .Lang "releases.assets_upload"}}</label>
50 - <input id="assets" name="assets" type="file" multiple class="input" style="padding:0.4rem 0.5rem;height:auto;">
51 - <div class="text-xs text-muted mt-1">{{t .Lang "releases.assets_upload_hint"}}</div>
48 + <div class="flex flex-col gap-1">
49 + <label class="label" for="assets" style="margin-bottom:0;">{{t .Lang "releases.assets_upload"}}</label>
50 + <label class="upload-drop" id="assets-drop" for="assets">
51 + {{icon "upload" "size-5"}}
52 + <span class="text-sm font-medium">{{t .Lang "file.upload_pick"}}</span>
53 + <span class="text-xs">{{t .Lang "releases.assets_upload_hint"}}</span>
54 + <input id="assets" name="assets" type="file" multiple style="display:none;">
55 + </label>
56 + <div id="assets-list" class="text-xs font-mono text-muted"></div>
52 57 </div>
53 58
54 59 <div>
... ... @@ -67,4 +72,30 @@
67 72 </form>
68 73 </section>
69 74 </div>
75 +<script>
76 +(function () {
77 + var input = document.getElementById('assets');
78 + var drop = document.getElementById('assets-drop');
79 + var list = document.getElementById('assets-list');
80 + if (!input || !drop || !list) return;
81 + function render() {
82 + var names = [];
83 + for (var i = 0; i < input.files.length; i++) names.push(input.files[i].name);
84 + list.textContent = names.length ? names.join(' · ') : '';
85 + }
86 + input.addEventListener('change', render);
87 + ['dragover', 'dragenter'].forEach(function (ev) {
88 + drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add('drag'); });
89 + });
90 + ['dragleave', 'drop'].forEach(function (ev) {
91 + drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove('drag'); });
92 + });
93 + drop.addEventListener('drop', function (e) {
94 + if (e.dataTransfer && e.dataTransfer.files.length) {
95 + input.files = e.dataTransfer.files;
96 + render();
97 + }
98 + });
99 +})();
100 +</script>
70 101 {{end}}
internal/web/views/partials/components.html
+11 -10
... ... @@ -43,11 +43,11 @@
43 43 {{end}}
44 44
45 45 {{define "repo_header"}}
46 - <div class="repo-banner mb-3">
46 + <div class="repo-banner mb-3{{if .Repo.ForkedFromRepoID}} has-subrow{{end}}">
47 47 <div class="repo-banner-main">
48 - <a href="/{{.Repo.OwnerUsername}}" class="flex-shrink-0">{{avatar .Repo.OwnerUsername .Repo.OwnerAvatarURL}}</a>
49 - <div class="min-w-0">
50 - <div class="flex items-center gap-2 flex-wrap">
48 + <a href="/{{.Repo.OwnerUsername}}" class="flex-shrink-0">{{avatar .Repo.OwnerUsername .Repo.OwnerAvatarURL "repo-avatar"}}</a>
49 + <div class="min-w-0 repo-head-block">
50 + <div class="repo-head-row">
51 51 <nav class="breadcrumb" aria-label="Repository breadcrumb">
52 52 <a href="/{{.Repo.OwnerUsername}}" class="text-sm font-medium text-muted">{{.Repo.OwnerUsername}}</a>
53 53 <span class="breadcrumb-sep">/</span>
... ... @@ -58,18 +58,19 @@
58 58 {{else}}
59 59 <span class="badge badge-outline">{{icon "globe" "size-3.5"}} {{t .Lang "repo.public"}}</span>
60 60 {{end}}
61 + {{if .Repo.Description}}
62 + <span class="repo-desc" data-repo-desc>{{.Repo.Description}}</span>
63 + {{end}}
61 64 </div>
62 - <div class="text-xs text-muted mt-1 flex items-center gap-2 flex-wrap">
63 - {{if .Repo.ForkedFromRepoID}}
65 + {{if .Repo.ForkedFromRepoID}}
66 + <div class="text-xs text-muted mt-1 flex items-center gap-2 flex-wrap">
64 67 <span class="flex items-center gap-1.5">
65 68 {{icon "git-branch" "size-3.5"}}
66 69 {{t .Lang "fork.forked_from"}}
67 70 <a class="font-medium" style="color:hsl(var(--foreground));" href="/{{.Repo.ForkedFromOwner}}/{{.Repo.ForkedFromName}}">{{.Repo.ForkedFromOwner}}/{{.Repo.ForkedFromName}}</a>
68 71 </span>
69 - {{else if .Repo.Description}}
70 - <span class="truncate" style="max-width:36rem;">{{.Repo.Description}}</span>
71 - {{end}}
72 - </div>
72 + </div>
73 + {{end}}
73 74 </div>
74 75 </div>
75 76