rin
Public Open-source lightweight git hosting.
app.js · 710 lines · 24.7 KB
Raw
1 (function () {
2 'use strict';
3
4 function showToast(message) {
5 var container = document.getElementById('toast-container');
6 if (!container) return;
7 var toast = document.createElement('div');
8 toast.className = 'toast';
9 toast.setAttribute('role', 'status');
10 toast.textContent = message;
11 container.appendChild(toast);
12 setTimeout(function () {
13 if (toast.parentNode) toast.parentNode.removeChild(toast);
14 }, 2800);
15 }
16 window.PolarToast = showToast;
17
18 function setTheme(theme, silent) {
19 var allowed = ['light', 'dark', 'system'];
20 if (allowed.indexOf(theme) === -1) theme = 'system';
21 document.documentElement.setAttribute('data-theme', theme);
22 try {
23 localStorage.setItem('polar_theme', theme);
24 } catch (e) {}
25 document.cookie = 'polar_theme=' + encodeURIComponent(theme) + '; path=/; max-age=31536000; SameSite=Lax';
26
27 document.querySelectorAll('[data-set-theme]').forEach(function (el) {
28 var isCurrent = el.getAttribute('data-set-theme') === theme;
29 el.classList.toggle('active', isCurrent);
30 el.setAttribute('aria-checked', isCurrent ? 'true' : 'false');
31 });
32 }
33 window.PolarSetTheme = setTheme;
34
35 try {
36 var currentAttr = document.documentElement.getAttribute('data-theme') || 'system';
37 document.querySelectorAll('[data-set-theme]').forEach(function (el) {
38 el.classList.toggle('active', el.getAttribute('data-set-theme') === currentAttr);
39 });
40 } catch (e) {}
41
42 function toggleSidebar() {
43 var html = document.documentElement;
44 var isCollapsed = html.getAttribute('data-sidebar') === 'collapsed';
45 if (isCollapsed) {
46 html.removeAttribute('data-sidebar');
47 try {
48 localStorage.setItem('polar_sidebar', 'expanded');
49 } catch (e) {}
50 } else {
51 html.setAttribute('data-sidebar', 'collapsed');
52 try {
53 localStorage.setItem('polar_sidebar', 'collapsed');
54 } catch (e) {}
55 }
56 }
57 window.PolarToggleSidebar = toggleSidebar;
58
59 function closeAllDropdowns() {
60 document.querySelectorAll('[data-dropdown-menu].open').forEach(function (menu) {
61 menu.classList.remove('open');
62 var parent = menu.closest('[data-dropdown]');
63 if (parent) {
64 var btn = parent.querySelector('[data-dropdown-trigger]');
65 if (btn) btn.setAttribute('aria-expanded', 'false');
66 }
67 });
68 }
69
70 function openDialog(id) {
71 var dlg = document.getElementById(id);
72 if (!dlg) return;
73 closeAllDropdowns();
74 dlg.classList.add('open');
75 dlg.setAttribute('aria-hidden', 'false');
76 var firstInput = dlg.querySelector('input:not([type="hidden"]), button[data-autofocus]');
77 if (firstInput) {
78 setTimeout(function () { firstInput.focus(); }, 20);
79 }
80 }
81 window.PolarOpenDialog = openDialog;
82
83 function closeDialog(dlg) {
84 if (!dlg) return;
85 dlg.classList.remove('open');
86 dlg.setAttribute('aria-hidden', 'true');
87 }
88
89 // Command Palette logic
90 var paletteIndex = 0;
91
92 function getVisiblePaletteItems() {
93 var list = document.querySelectorAll('#command-palette [data-palette-item]');
94 var visible = [];
95 list.forEach(function (item) {
96 if (item.style.display !== 'none') visible.push(item);
97 });
98 return visible;
99 }
100
101 function updatePaletteSelection() {
102 var items = getVisiblePaletteItems();
103 if (paletteIndex >= items.length) paletteIndex = 0;
104 if (paletteIndex < 0) paletteIndex = Math.max(0, items.length - 1);
105 items.forEach(function (el, idx) {
106 el.classList.toggle('selected', idx === paletteIndex);
107 if (idx === paletteIndex) {
108 el.scrollIntoView({ block: 'nearest' });
109 }
110 });
111 }
112
113 function filterPalette(query) {
114 var q = (query || '').trim().toLowerCase();
115 var allItems = document.querySelectorAll('#command-palette [data-palette-item]');
116 allItems.forEach(function (item) {
117 var text = (item.getAttribute('data-palette-keywords') || item.textContent || '').toLowerCase();
118 var show = q === '' || text.indexOf(q) !== -1;
119 item.style.display = show ? '' : 'none';
120 });
121 document.querySelectorAll('#command-palette [data-palette-group]').forEach(function (group) {
122 var hasVisible = false;
123 group.querySelectorAll('[data-palette-item]').forEach(function (it) {
124 if (it.style.display !== 'none') hasVisible = true;
125 });
126 group.style.display = hasVisible ? '' : 'none';
127 });
128 paletteIndex = 0;
129 updatePaletteSelection();
130 }
131
132 function openPalette(initialQuery) {
133 var palette = document.getElementById('command-palette');
134 if (!palette) return;
135 closeAllDropdowns();
136 palette.classList.add('open');
137 palette.setAttribute('aria-hidden', 'false');
138 var input = document.getElementById('command-palette-input');
139 if (input) {
140 input.value = initialQuery || '';
141 filterPalette(input.value);
142 setTimeout(function () {
143 input.focus();
144 if (initialQuery) input.select();
145 }, 20);
146 }
147 }
148 window.PolarOpenPalette = openPalette;
149
150 function activatePaletteItem(item) {
151 if (!item) return;
152 var palette = document.getElementById('command-palette');
153 var href = item.getAttribute('data-palette-href');
154 var theme = item.getAttribute('data-palette-theme');
155 var action = item.getAttribute('data-palette-action');
156 if (palette) closeDialog(palette);
157
158 if (theme) {
159 setTheme(theme, false);
160 } else if (action === 'shortcuts') {
161 openDialog('shortcuts-dialog');
162 } else if (action === 'toggle-sidebar') {
163 toggleSidebar();
164 } else if (href) {
165 window.location.href = href;
166 }
167 }
168
169 document.addEventListener('click', function (e) {
170 // Toggle sidebar
171 var sbBtn = e.target.closest('[data-toggle-sidebar]');
172 if (sbBtn) {
173 e.preventDefault();
174 toggleSidebar();
175 return;
176 }
177
178 // Dropdown trigger
179 var trigger = e.target.closest('[data-dropdown-trigger]');
180 if (trigger) {
181 e.stopPropagation();
182 var dropdown = trigger.closest('[data-dropdown]');
183 var menu = dropdown ? dropdown.querySelector('[data-dropdown-menu]') : null;
184 var wasOpen = menu && menu.classList.contains('open');
185 closeAllDropdowns();
186 if (menu && !wasOpen) {
187 menu.classList.add('open');
188 trigger.setAttribute('aria-expanded', 'true');
189 }
190 return;
191 }
192
193 // Theme button
194 var themeBtn = e.target.closest('[data-set-theme]');
195 if (themeBtn) {
196 e.preventDefault();
197 setTheme(themeBtn.getAttribute('data-set-theme'), false);
198 closeAllDropdowns();
199 return;
200 }
201
202 // Open Command Palette
203 var paletteTrigger = e.target.closest('[data-open-palette]');
204 if (paletteTrigger) {
205 e.preventDefault();
206 openPalette(paletteTrigger.getAttribute('data-open-palette') || '');
207 return;
208 }
209
210 // Palette item click
211 var paletteItem = e.target.closest('[data-palette-item]');
212 if (paletteItem) {
213 e.preventDefault();
214 activatePaletteItem(paletteItem);
215 return;
216 }
217
218 // Dialog open
219 var dlgOpen = e.target.closest('[data-dialog-open]');
220 if (dlgOpen) {
221 e.preventDefault();
222 openDialog(dlgOpen.getAttribute('data-dialog-open'));
223 return;
224 }
225
226 // Dialog close
227 var dlgClose = e.target.closest('[data-dialog-close]');
228 if (dlgClose) {
229 e.preventDefault();
230 closeDialog(dlgClose.closest('.backdrop'));
231 return;
232 }
233
234 // Backdrop click
235 if (e.target.classList.contains('backdrop')) {
236 closeDialog(e.target);
237 return;
238 }
239
240 // Mobile drawer
241 var drawerOpen = e.target.closest('[data-drawer-open]');
242 if (drawerOpen) {
243 var drw = document.getElementById('mobile-drawer');
244 if (drw) drw.classList.add('open');
245 return;
246 }
247 var drawerClose = e.target.closest('[data-drawer-close]');
248 if (drawerClose || e.target.id === 'mobile-drawer') {
249 var drw2 = document.getElementById('mobile-drawer');
250 if (drw2) drw2.classList.remove('open');
251 return;
252 }
253
254 // Copy to clipboard
255 var copyBtn = e.target.closest('[data-copy]');
256 if (copyBtn) {
257 e.preventDefault();
258 var text = copyBtn.getAttribute('data-copy');
259 var copiedMsg = document.documentElement.getAttribute('data-copied-text') || 'Copied';
260 if (navigator.clipboard && navigator.clipboard.writeText) {
261 navigator.clipboard.writeText(text).then(function () {
262 showToast(copiedMsg);
263 });
264 } else {
265 var ta = document.createElement('textarea');
266 ta.value = text;
267 document.body.appendChild(ta);
268 ta.select();
269 document.execCommand('copy');
270 document.body.removeChild(ta);
271 showToast(copiedMsg);
272 }
273 return;
274 }
275
276 if (!e.target.closest('[data-dropdown-menu]')) {
277 closeAllDropdowns();
278 }
279 });
280
281 // Input handlers (palette filter + delete repo confirmation match)
282 document.addEventListener('input', function (e) {
283 if (e.target.id === 'command-palette-input') {
284 filterPalette(e.target.value);
285 return;
286 }
287
288 var matchExpected = e.target.getAttribute('data-confirm-input');
289 if (matchExpected !== null) {
290 var btnId = e.target.getAttribute('data-confirm-button');
291 var btn = btnId ? document.getElementById(btnId) : null;
292 if (btn) {
293 btn.disabled = e.target.value.trim() !== matchExpected;
294 }
295 }
296
297 if (e.target.hasAttribute('data-file-filter')) {
298 var q = e.target.value.trim().toLowerCase();
299 document.querySelectorAll('[data-tree-row]').forEach(function (row) {
300 var name = (row.getAttribute('data-tree-name') || '').toLowerCase();
301 row.style.display = !q || name.indexOf(q) !== -1 ? '' : 'none';
302 });
303 }
304 });
305
306 // Keyboard navigation & chords
307 var lastChordKey = '';
308 var lastChordTime = 0;
309
310 document.addEventListener('keydown', function (e) {
311 var palette = document.getElementById('command-palette');
312 var isPaletteOpen = palette && palette.classList.contains('open');
313
314 if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
315 e.preventDefault();
316 if (isPaletteOpen) {
317 closeDialog(palette);
318 } else {
319 openPalette('');
320 }
321 return;
322 }
323
324 if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'b') {
325 e.preventDefault();
326 toggleSidebar();
327 return;
328 }
329
330 if (e.key === 'Escape') {
331 closeAllDropdowns();
332 document.querySelectorAll('.backdrop.open').forEach(function (b) {
333 closeDialog(b);
334 });
335 var drw = document.getElementById('mobile-drawer');
336 if (drw) drw.classList.remove('open');
337 return;
338 }
339
340 if (isPaletteOpen) {
341 var items = getVisiblePaletteItems();
342 if (e.key === 'ArrowDown') {
343 e.preventDefault();
344 paletteIndex = (paletteIndex + 1) % Math.max(1, items.length);
345 updatePaletteSelection();
346 } else if (e.key === 'ArrowUp') {
347 e.preventDefault();
348 paletteIndex = (paletteIndex - 1 + Math.max(1, items.length)) % Math.max(1, items.length);
349 updatePaletteSelection();
350 } else if (e.key === 'Enter') {
351 e.preventDefault();
352 if (items[paletteIndex]) activatePaletteItem(items[paletteIndex]);
353 }
354 return;
355 }
356
357 var tag = e.target && e.target.tagName ? e.target.tagName.toUpperCase() : '';
358 if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target && e.target.isContentEditable)) {
359 return;
360 }
361 if (e.ctrlKey || e.metaKey || e.altKey) return;
362
363 var now = Date.now();
364 if (lastChordKey === 'g' && now - lastChordTime < 900) {
365 lastChordKey = '';
366 if (e.key.toLowerCase() === 'h') {
367 e.preventDefault();
368 window.location.href = '/';
369 return;
370 }
371 if (e.key.toLowerCase() === 'r') {
372 e.preventDefault();
373 window.location.href = '/explore';
374 return;
375 }
376 }
377
378 if (e.key.toLowerCase() === 'g') {
379 lastChordKey = 'g';
380 lastChordTime = now;
381 return;
382 }
383
384 if (e.key === '/') {
385 e.preventDefault();
386 var searchInput = document.querySelector('[data-search-input]');
387 if (searchInput) {
388 searchInput.focus();
389 } else {
390 openPalette('');
391 }
392 return;
393 }
394
395 if (e.key === '?') {
396 e.preventDefault();
397 openDialog('shortcuts-dialog');
398 return;
399 }
400
401 if (e.key.toLowerCase() === 't') {
402 e.preventDefault();
403 var fileFilter = document.querySelector('[data-file-filter]');
404 if (fileFilter) {
405 fileFilter.focus();
406 } else {
407 openPalette('file:');
408 }
409 }
410 });
411
412 // Avatar file picker preview & client-side 256x256 square crop/resize
413 document.addEventListener('change', function (e) {
414 if (e.target && e.target.id === 'avatar-file-input') {
415 var file = e.target.files && e.target.files[0];
416 if (!file) return;
417 var reader = new FileReader();
418 reader.onload = function (ev) {
419 var img = new Image();
420 img.onload = function () {
421 var size = 256;
422 var canvas = document.createElement('canvas');
423 canvas.width = size;
424 canvas.height = size;
425 var ctx = canvas.getContext('2d');
426 var minSide = Math.min(img.width, img.height);
427 var sx = (img.width - minSide) / 2;
428 var sy = (img.height - minSide) / 2;
429 ctx.drawImage(img, sx, sy, minSide, minSide, 0, 0, size, size);
430 var dataUrl = canvas.toDataURL('image/webp', 0.88);
431 if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) {
432 dataUrl = canvas.toDataURL('image/jpeg', 0.88);
433 }
434 var hidden = document.getElementById('avatar-data-input');
435 var rmHidden = document.getElementById('remove-avatar-input');
436 if (hidden) hidden.value = dataUrl;
437 if (rmHidden) rmHidden.value = 'false';
438 var previewWrap = document.getElementById('avatar-preview-wrap');
439 if (previewWrap) {
440 previewWrap.innerHTML = '<span class="avatar avatar-xl"><img src="' + dataUrl + '" alt="Avatar" class="avatar-img"></span>';
441 }
442 };
443 img.src = ev.target.result;
444 };
445 reader.readAsDataURL(file);
446 }
447 });
448
449 document.addEventListener('click', function (e) {
450 var rmBtn = e.target.closest('#remove-avatar-btn');
451 if (rmBtn) {
452 e.preventDefault();
453 var rmHidden = document.getElementById('remove-avatar-input');
454 var hidden = document.getElementById('avatar-data-input');
455 var fileInput = document.getElementById('avatar-file-input');
456 if (rmHidden) rmHidden.value = 'true';
457 if (hidden) hidden.value = '';
458 if (fileInput) fileInput.value = '';
459 var previewWrap = document.getElementById('avatar-preview-wrap');
460 if (previewWrap) {
461 previewWrap.innerHTML = '<span class="avatar avatar-xl">?</span>';
462 }
463 rmBtn.style.display = 'none';
464 }
465 });
466
467 function resizeCoverImage(file, width, height, callback) {
468 var reader = new FileReader();
469 reader.onload = function (ev) {
470 var img = new Image();
471 img.onload = function () {
472 var canvas = document.createElement('canvas');
473 canvas.width = width;
474 canvas.height = height;
475 var ctx = canvas.getContext('2d');
476 var scale = Math.max(width / img.width, height / img.height);
477 var sw = width / scale;
478 var sh = height / scale;
479 var sx = (img.width - sw) / 2;
480 var sy = (img.height - sh) / 2;
481 ctx.drawImage(img, sx, sy, sw, sh, 0, 0, width, height);
482 var dataUrl = canvas.toDataURL('image/webp', 0.84);
483 if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) {
484 dataUrl = canvas.toDataURL('image/jpeg', 0.84);
485 }
486 callback(dataUrl);
487 };
488 img.src = ev.target.result;
489 };
490 reader.readAsDataURL(file);
491 }
492
493 function resizeSquareImage(file, size, callback) {
494 var reader = new FileReader();
495 reader.onload = function (ev) {
496 var img = new Image();
497 img.onload = function () {
498 var canvas = document.createElement('canvas');
499 canvas.width = size;
500 canvas.height = size;
501 var ctx = canvas.getContext('2d');
502 var minSide = Math.min(img.width, img.height);
503 var sx = (img.width - minSide) / 2;
504 var sy = (img.height - minSide) / 2;
505 ctx.drawImage(img, sx, sy, minSide, minSide, 0, 0, size, size);
506 var dataUrl = canvas.toDataURL('image/webp', 0.88);
507 if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) {
508 dataUrl = canvas.toDataURL('image/jpeg', 0.88);
509 }
510 callback(dataUrl);
511 };
512 img.src = ev.target.result;
513 };
514 reader.readAsDataURL(file);
515 }
516
517 document.addEventListener('change', function (e) {
518 if (!e.target) return;
519 if (e.target.id === 'background-file-input') {
520 var bgFile = e.target.files && e.target.files[0];
521 if (!bgFile) return;
522 resizeCoverImage(bgFile, 1600, 700, function (dataUrl) {
523 var hidden = document.getElementById('background-data-input');
524 var remove = document.getElementById('remove-background-input');
525 var preview = document.getElementById('background-preview');
526 if (hidden) hidden.value = dataUrl;
527 if (remove) remove.value = 'false';
528 if (preview) {
529 preview.classList.remove('is-empty');
530 preview.innerHTML = '<img src="' + dataUrl + '" alt="">';
531 }
532 });
533 return;
534 }
535 if (e.target.id === 'repo-background-file-input') {
536 var repoBgFile = e.target.files && e.target.files[0];
537 if (!repoBgFile) return;
538 resizeCoverImage(repoBgFile, 1600, 700, function (dataUrl) {
539 var hidden = document.getElementById('repo-background-data-input');
540 var remove = document.getElementById('remove-repo-background-input');
541 var preview = document.getElementById('repo-background-preview');
542 if (hidden) hidden.value = dataUrl;
543 if (remove) remove.value = 'false';
544 if (preview) {
545 preview.classList.remove('is-empty');
546 preview.innerHTML = '<img src="' + dataUrl + '" alt="">';
547 }
548 });
549 return;
550 }
551 if (e.target.id === 'repo-avatar-file-input') {
552 var repoFile = e.target.files && e.target.files[0];
553 if (!repoFile) return;
554 resizeSquareImage(repoFile, 256, function (dataUrl) {
555 var hidden = document.getElementById('repo-avatar-data-input');
556 var remove = document.getElementById('remove-repo-avatar-input');
557 var preview = document.getElementById('repo-avatar-preview-wrap');
558 if (hidden) hidden.value = dataUrl;
559 if (remove) remove.value = 'false';
560 if (preview) {
561 preview.innerHTML = '<span class="avatar avatar-xl"><img src="' + dataUrl + '" alt="Repository avatar" class="avatar-img"></span>';
562 }
563 });
564 }
565 });
566
567 document.addEventListener('click', function (e) {
568 var removeBg = e.target.closest('#remove-background-btn');
569 if (removeBg) {
570 e.preventDefault();
571 var hidden = document.getElementById('background-data-input');
572 var remove = document.getElementById('remove-background-input');
573 var fileInput = document.getElementById('background-file-input');
574 var preview = document.getElementById('background-preview');
575 if (hidden) hidden.value = '';
576 if (remove) remove.value = 'true';
577 if (fileInput) fileInput.value = '';
578 if (preview) {
579 preview.classList.add('is-empty');
580 preview.innerHTML = '<span class="background-preview-placeholder"><svg class="icon size-5" aria-hidden="true"><use href="#hi-image"></use></svg></span>';
581 }
582 removeBg.style.display = 'none';
583 return;
584 }
585
586 var removeRepoBg = e.target.closest('#remove-repo-background-btn');
587 if (removeRepoBg) {
588 e.preventDefault();
589 var hidden = document.getElementById('repo-background-data-input');
590 var remove = document.getElementById('remove-repo-background-input');
591 var fileInput = document.getElementById('repo-background-file-input');
592 var preview = document.getElementById('repo-background-preview');
593 if (hidden) hidden.value = '';
594 if (remove) remove.value = 'true';
595 if (fileInput) fileInput.value = '';
596 if (preview) {
597 preview.classList.add('is-empty');
598 preview.innerHTML = '<span class="background-preview-placeholder"><svg class="icon size-5" aria-hidden="true"><use href="#hi-image"></use></svg></span>';
599 }
600 removeRepoBg.style.display = 'none';
601 return;
602 }
603
604 var removeRepoAvatar = e.target.closest('#remove-repo-avatar-btn');
605 if (removeRepoAvatar) {
606 e.preventDefault();
607 var hidden = document.getElementById('repo-avatar-data-input');
608 var remove = document.getElementById('remove-repo-avatar-input');
609 var fileInput = document.getElementById('repo-avatar-file-input');
610 var preview = document.getElementById('repo-avatar-preview-wrap');
611 if (hidden) hidden.value = '';
612 if (remove) remove.value = 'true';
613 if (fileInput) fileInput.value = '';
614 if (preview) preview.innerHTML = '<span class="avatar avatar-xl">?</span>';
615 removeRepoAvatar.style.display = 'none';
616 return;
617 }
618
619 var deleteUser = e.target.closest('[data-admin-delete-user]');
620 if (deleteUser) {
621 e.preventDefault();
622 var name = deleteUser.getAttribute('data-admin-delete-name') || '';
623 var userForm = document.getElementById('admin-delete-user-form');
624 var userInput = document.getElementById('admin_delete_confirm_user');
625 var userTarget = document.getElementById('admin-delete-user-target');
626 var userSubmit = document.getElementById('admin-delete-user-submit');
627 if (userForm) userForm.setAttribute('action', '/admin/users/' + deleteUser.getAttribute('data-admin-delete-user') + '/delete');
628 if (userTarget) userTarget.textContent = '@' + name;
629 if (userInput) {
630 userInput.value = '';
631 userInput.setAttribute('data-confirm-input', name);
632 }
633 if (userSubmit) userSubmit.disabled = true;
634 openDialog('admin-delete-user-dialog');
635 return;
636 }
637
638 var deleteRepo = e.target.closest('[data-admin-delete-repo]');
639 if (deleteRepo) {
640 e.preventDefault();
641 var repoName = deleteRepo.getAttribute('data-admin-delete-name') || '';
642 var repoForm = document.getElementById('admin-delete-repo-form');
643 var repoInput = document.getElementById('admin_delete_confirm_repo');
644 var repoTarget = document.getElementById('admin-delete-repo-target');
645 var repoSubmit = document.getElementById('admin-delete-repo-submit');
646 if (repoForm) repoForm.setAttribute('action', '/admin/repos/' + deleteRepo.getAttribute('data-admin-delete-repo') + '/delete');
647 if (repoTarget) repoTarget.textContent = repoName;
648 if (repoInput) {
649 repoInput.value = '';
650 repoInput.setAttribute('data-confirm-input', repoName);
651 }
652 if (repoSubmit) repoSubmit.disabled = true;
653 openDialog('admin-delete-repo-dialog');
654 }
655 });
656
657 // Repo header: the description stays on the title line and expands onto its
658 // own line when clicked. The avatar is sized to the title line so it always
659 // sits on the same row as the repository name, at a modest size.
660 var repoAvatarTimer = null;
661
662 function sizeRepoAvatars() {
663 var changed = false;
664 document.querySelectorAll('.repo-banner').forEach(function (banner) {
665 var avatar = banner.querySelector('.repo-avatar');
666 var title = banner.querySelector('.repo-head-row nav');
667 if (!avatar || !title) return;
668 var h = Math.round(title.getBoundingClientRect().height);
669 if (h <= 0) return;
670 var size = Math.min(30, Math.max(18, h));
671 var sizePx = size + 'px';
672 if (avatar.style.width === sizePx) return;
673 avatar.style.width = sizePx;
674 avatar.style.height = sizePx;
675 avatar.style.fontSize = Math.min(20, Math.max(9, Math.round(size * 0.42))) + 'px';
676 changed = true;
677 });
678 return changed;
679 }
680
681 function updateRepoDescs() {
682 document.querySelectorAll('[data-repo-desc]').forEach(function (el) {
683 var banner = el.closest('.repo-banner');
684 if (banner && banner.classList.contains('desc-open')) {
685 el.classList.add('truncated');
686 return;
687 }
688 el.classList.toggle('truncated', el.scrollWidth > el.clientWidth + 1);
689 });
690 if (sizeRepoAvatars()) {
691 // Re-measure once the width transition settles: the final avatar width
692 // changes the room left for the description.
693 window.clearTimeout(repoAvatarTimer);
694 repoAvatarTimer = window.setTimeout(sizeRepoAvatars, 300);
695 }
696 }
697
698 document.addEventListener('click', function (e) {
699 var desc = e.target.closest('[data-repo-desc]');
700 if (!desc || !desc.classList.contains('truncated')) return;
701 var banner = desc.closest('.repo-banner');
702 if (!banner) return;
703 banner.classList.toggle('desc-open');
704 updateRepoDescs();
705 });
706
707 window.addEventListener('resize', updateRepoDescs);
708 document.addEventListener('htmx:afterSwap', updateRepoDescs);
709 updateRepoDescs();
710 })();