(function () {
'use strict';
function showToast(message) {
var container = document.getElementById('toast-container');
if (!container) return;
var toast = document.createElement('div');
toast.className = 'toast';
toast.setAttribute('role', 'status');
toast.textContent = message;
container.appendChild(toast);
setTimeout(function () {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 2800);
}
window.PolarToast = showToast;
function copyText(text, successMessage) {
var value = text == null ? '' : String(text);
if (!value) return;
var copiedMsg = successMessage || document.documentElement.getAttribute('data-copied-text') || 'Copied';
var failedMsg = document.documentElement.getAttribute('data-copy-failed-text') || 'Could not copy';
function fallbackCopy() {
var previousFocus = document.activeElement;
var ta = document.createElement('textarea');
ta.value = value;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.left = '-9999px';
ta.style.top = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
ta.setSelectionRange(0, value.length);
var ok = false;
try {
ok = document.execCommand('copy');
} catch (err) {
ok = false;
}
if (ta.parentNode) ta.parentNode.removeChild(ta);
if (previousFocus && previousFocus.focus) {
try { previousFocus.focus({ preventScroll: true }); } catch (err2) {}
}
showToast(ok ? copiedMsg : failedMsg);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value).then(function () {
showToast(copiedMsg);
}, fallbackCopy);
} else {
fallbackCopy();
}
}
function setTheme(theme, silent) {
var allowed = ['light', 'dark', 'system'];
if (allowed.indexOf(theme) === -1) theme = 'system';
document.documentElement.setAttribute('data-theme', theme);
try {
localStorage.setItem('polar_theme', theme);
} catch (e) {}
document.cookie = 'polar_theme=' + encodeURIComponent(theme) + '; path=/; max-age=31536000; SameSite=Lax';
document.querySelectorAll('[data-set-theme]').forEach(function (el) {
var isCurrent = el.getAttribute('data-set-theme') === theme;
el.classList.toggle('active', isCurrent);
el.setAttribute('aria-checked', isCurrent ? 'true' : 'false');
});
}
window.PolarSetTheme = setTheme;
try {
var currentAttr = document.documentElement.getAttribute('data-theme') || 'system';
document.querySelectorAll('[data-set-theme]').forEach(function (el) {
el.classList.toggle('active', el.getAttribute('data-set-theme') === currentAttr);
});
} catch (e) {}
function toggleSidebar() {
var html = document.documentElement;
var isCollapsed = html.getAttribute('data-sidebar') === 'collapsed';
if (isCollapsed) {
html.removeAttribute('data-sidebar');
try {
localStorage.setItem('polar_sidebar', 'expanded');
} catch (e) {}
} else {
html.setAttribute('data-sidebar', 'collapsed');
try {
localStorage.setItem('polar_sidebar', 'collapsed');
} catch (e) {}
}
}
window.PolarToggleSidebar = toggleSidebar;
function closeAllDropdowns() {
document.querySelectorAll('[data-dropdown-menu].open').forEach(function (menu) {
menu.classList.remove('open');
var parent = menu.closest('[data-dropdown]');
if (parent) {
var btn = parent.querySelector('[data-dropdown-trigger]');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
});
}
var contextMenuIndex = -1;
var contextMenuState = null;
function contextElement(target) {
if (!target) return null;
if (target.nodeType === 1) return target;
return target.parentElement || null;
}
function isEditableTarget(target) {
if (!target || !target.tagName) return false;
var tag = target.tagName.toUpperCase();
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable;
}
function getContextMenuItems() {
var menu = document.getElementById('context-menu');
if (!menu) return [];
return Array.prototype.filter.call(menu.querySelectorAll('.context-menu-item'), function (item) {
var section = item.closest('[data-context-section]');
return !section || !section.hidden;
});
}
function focusContextMenuItem(index) {
var items = getContextMenuItems();
if (!items.length) return;
contextMenuIndex = (index + items.length) % items.length;
items.forEach(function (item, idx) {
item.classList.toggle('is-active', idx === contextMenuIndex);
});
try {
items[contextMenuIndex].focus({ preventScroll: true });
} catch (err) {
items[contextMenuIndex].focus();
}
}
function closeContextMenu() {
var menu = document.getElementById('context-menu');
if (!menu || !menu.classList.contains('open')) return;
var restoreFocus = contextMenuState && contextMenuState.restoreFocus;
menu.classList.remove('open');
menu.setAttribute('aria-hidden', 'true');
menu.style.left = '';
menu.style.top = '';
contextMenuState = null;
contextMenuIndex = -1;
menu.querySelectorAll('.context-menu-item.is-active').forEach(function (item) {
item.classList.remove('is-active');
});
if (restoreFocus && restoreFocus !== document.body && restoreFocus.isConnected) {
try { restoreFocus.focus({ preventScroll: true }); } catch (err) {}
}
}
function openContextMenuAt(x, y, target, restoreFocus) {
var menu = document.getElementById('context-menu');
var el = contextElement(target);
if (!menu || !el) return;
closeAllDropdowns();
var link = el.closest ? el.closest('a[href]') : null;
if (link && (!link.getAttribute('href') || /^javascript:/i.test(link.getAttribute('href')))) {
link = null;
}
var image = el.closest ? el.closest('img') : null;
var imageURL = image ? (image.currentSrc || image.src || '') : '';
var imageIsData = /^data:/i.test(imageURL);
var imageIsTemporary = /^(data|blob):/i.test(imageURL);
var selectedText = '';
try {
var selection = window.getSelection();
if (selection && !selection.isCollapsed) selectedText = selection.toString().trim();
} catch (err) {}
var searchText = selectedText.replace(/\s+/g, ' ').trim().slice(0, 160);
var linkText = '';
if (link) {
linkText = (link.innerText || link.textContent || link.getAttribute('aria-label') || link.getAttribute('title') || '')
.replace(/\s+/g, ' ').trim();
}
contextMenuState = {
linkHref: link ? link.href : '',
linkText: linkText,
imageURL: imageURL,
imageIsData: imageIsData,
selectedText: selectedText,
searchText: searchText,
restoreFocus: restoreFocus || document.activeElement
};
['selection', 'link', 'image'].forEach(function (name) {
var section = menu.querySelector('[data-context-section="' + name + '"]');
if (section) {
section.hidden = name === 'selection' ? !selectedText : name === 'link' ? !link : !imageURL;
}
});
var linkTextItem = menu.querySelector('[data-context-action="link-text"]');
if (linkTextItem) linkTextItem.hidden = !linkText;
var imageOpenItem = menu.querySelector('[data-context-action="image-open"]');
if (imageOpenItem) imageOpenItem.hidden = !imageURL || imageIsData;
var imageCopyItem = menu.querySelector('[data-context-action="image-copy"]');
if (imageCopyItem) imageCopyItem.hidden = !imageURL || imageIsTemporary;
menu.classList.add('open');
menu.setAttribute('aria-hidden', 'false');
menu.scrollTop = 0;
menu.style.left = '0px';
menu.style.top = '0px';
var menuWidth = menu.offsetWidth;
var menuHeight = menu.offsetHeight;
var gap = 8;
var vw = window.innerWidth || document.documentElement.clientWidth;
var vh = window.innerHeight || document.documentElement.clientHeight;
var left = x;
var top = y;
if (left + menuWidth > vw - gap) left = x - menuWidth;
if (left < gap) left = gap;
if (left + menuWidth > vw - gap) left = Math.max(gap, vw - menuWidth - gap);
if (top + menuHeight > vh - gap) top = y - menuHeight;
if (top < gap) top = gap;
if (top + menuHeight > vh - gap) top = Math.max(gap, vh - menuHeight - gap);
menu.style.left = Math.round(left) + 'px';
menu.style.top = Math.round(top) + 'px';
contextMenuIndex = -1;
setTimeout(function () {
if (menu.classList.contains('open')) focusContextMenuItem(0);
}, 20);
}
function openContextMenuForKeyboard() {
var target = document.activeElement;
if (!target || target === document.body) target = document.body;
var rect = target.getBoundingClientRect ? target.getBoundingClientRect() : null;
var x = rect ? rect.left + Math.min(rect.width, 20) : 16;
var y = rect ? rect.bottom + 4 : 16;
openContextMenuAt(x, y, target, target);
}
function runContextMenuAction(item) {
var action = item.getAttribute('data-context-action');
var state = contextMenuState || {};
closeContextMenu();
if (action === 'copy-selection') {
copyText(state.selectedText);
} else if (action === 'search-selection') {
openPalette(state.searchText || state.selectedText || '');
} else if (action === 'link-open') {
if (state.linkHref) window.open(state.linkHref, '_blank', 'noopener,noreferrer');
} else if (action === 'link-copy') {
copyText(state.linkHref);
} else if (action === 'link-text') {
copyText(state.linkText);
} else if (action === 'image-open') {
if (state.imageURL) window.open(state.imageURL, '_blank', 'noopener,noreferrer');
} else if (action === 'image-copy') {
copyText(state.imageURL);
} else if (action === 'image-save') {
if (state.imageURL) {
var extension = 'img';
var dataMatch = state.imageURL.match(/^data:image\/([a-z0-9.+-]+)/i);
if (dataMatch) {
extension = dataMatch[1].toLowerCase().replace('jpeg', 'jpg').replace('svg+xml', 'svg');
} else {
try {
var pathMatch = new URL(state.imageURL).pathname.match(/\.([a-z0-9]{2,5})$/i);
if (pathMatch) extension = pathMatch[1].toLowerCase();
} catch (err) {}
}
var download = document.createElement('a');
download.href = state.imageURL;
download.download = 'polar-image.' + extension;
document.body.appendChild(download);
download.click();
document.body.removeChild(download);
}
} else if (action === 'page-copy') {
copyText(window.location.href);
} else if (action === 'page-reload') {
window.location.reload();
} else if (action === 'page-back') {
window.history.back();
} else if (action === 'page-forward') {
window.history.forward();
}
}
function cycleTheme() {
var current = document.documentElement.getAttribute('data-theme') || 'system';
var order = ['light', 'dark', 'system'];
var next = order[(order.indexOf(current) + 1) % order.length];
setTheme(next, false);
var item = document.querySelector('#command-palette [data-palette-theme="' + next + '"]');
showToast(item ? item.textContent.trim() : next);
}
function shortcutUnavailable() {
showToast(document.documentElement.getAttribute('data-shortcut-unavailable') || 'Shortcut unavailable here');
}
function handleNavigationChord(key) {
var body = document.body;
var repoPath = body ? body.getAttribute('data-repo-path') : '';
var currentUser = body ? body.getAttribute('data-current-user') : '';
var repoFlags = {
issues: body && body.getAttribute('data-repo-issues') === 'true',
pulls: body && body.getAttribute('data-repo-pulls') === 'true',
releases: body && body.getAttribute('data-repo-releases') === 'true',
activity: body && body.getAttribute('data-repo-activity') === 'true',
canWrite: body && body.getAttribute('data-repo-can-write') === 'true'
};
if (key === 'h') {
window.location.href = '/';
return true;
}
if (key === 'r') {
window.location.href = '/explore';
return true;
}
if (key === 'o') {
if (!currentUser) {
shortcutUnavailable();
return true;
}
window.location.href = '/' + encodeURIComponent(currentUser);
return true;
}
if (key === 'n') {
if (!currentUser) {
shortcutUnavailable();
return true;
}
window.location.href = '/new';
return true;
}
if (key === 's' && !repoPath) {
if (currentUser) {
window.location.href = '/settings';
} else {
shortcutUnavailable();
}
return true;
}
if (!repoPath) {
var isRepoChord = 'ciplab'.indexOf(key) !== -1;
if (isRepoChord) shortcutUnavailable();
return isRepoChord;
}
if (key === 'c') {
window.location.href = repoPath;
return true;
}
if (key === 'i') {
if (!repoFlags.issues) shortcutUnavailable();
else window.location.href = repoPath + '/issues';
return true;
}
if (key === 'p') {
if (!repoFlags.pulls) shortcutUnavailable();
else window.location.href = repoPath + '/pulls';
return true;
}
if (key === 'l') {
if (!repoFlags.releases) shortcutUnavailable();
else window.location.href = repoPath + '/releases';
return true;
}
if (key === 'a') {
if (!repoFlags.activity) shortcutUnavailable();
else window.location.href = repoPath + '/activity';
return true;
}
if (key === 'b') {
window.location.href = repoPath + '/branches';
return true;
}
if (key === 's') {
if (!repoFlags.canWrite) shortcutUnavailable();
else window.location.href = repoPath + '/settings';
return true;
}
return false;
}
document.addEventListener('contextmenu', function (e) {
var el = contextElement(e.target);
if (!el || el.closest('#context-menu') || el.isContentEditable || el.closest('input, textarea, select, [data-native-context]')) {
return;
}
e.preventDefault();
openContextMenuAt(e.clientX, e.clientY, el, document.activeElement);
});
document.addEventListener('pointerdown', function (e) {
var menu = document.getElementById('context-menu');
if (!menu || !menu.classList.contains('open')) return;
if (!e.target.closest || !e.target.closest('#context-menu')) closeContextMenu();
}, true);
document.addEventListener('scroll', function (e) {
var target = contextElement(e.target);
if (target && target.closest && target.closest('#context-menu')) return;
closeContextMenu();
}, true);
window.addEventListener('resize', closeContextMenu);
window.addEventListener('popstate', closeContextMenu);
window.addEventListener('hashchange', closeContextMenu);
document.addEventListener('htmx:beforeSwap', closeContextMenu);
function openDialog(id) {
var dlg = document.getElementById(id);
if (!dlg) return;
closeAllDropdowns();
closeContextMenu();
dlg.classList.add('open');
dlg.setAttribute('aria-hidden', 'false');
var firstInput = dlg.querySelector('input:not([type="hidden"]), button[data-autofocus]');
if (firstInput) {
setTimeout(function () { firstInput.focus(); }, 20);
}
}
window.PolarOpenDialog = openDialog;
function closeDialog(dlg) {
if (!dlg) return;
dlg.classList.remove('open');
dlg.setAttribute('aria-hidden', 'true');
}
// Command Palette logic
var paletteIndex = 0;
function getVisiblePaletteItems() {
var list = document.querySelectorAll('#command-palette [data-palette-item]');
var visible = [];
list.forEach(function (item) {
if (item.style.display !== 'none') visible.push(item);
});
return visible;
}
function updatePaletteSelection() {
var items = getVisiblePaletteItems();
if (paletteIndex >= items.length) paletteIndex = 0;
if (paletteIndex < 0) paletteIndex = Math.max(0, items.length - 1);
items.forEach(function (el, idx) {
el.classList.toggle('selected', idx === paletteIndex);
if (idx === paletteIndex) {
el.scrollIntoView({ block: 'nearest' });
}
});
}
function filterPalette(query) {
var q = (query || '').trim().toLowerCase();
var allItems = document.querySelectorAll('#command-palette [data-palette-item]');
allItems.forEach(function (item) {
var text = (item.getAttribute('data-palette-keywords') || item.textContent || '').toLowerCase();
var show = q === '' || text.indexOf(q) !== -1;
item.style.display = show ? '' : 'none';
});
document.querySelectorAll('#command-palette [data-palette-group]').forEach(function (group) {
var hasVisible = false;
group.querySelectorAll('[data-palette-item]').forEach(function (it) {
if (it.style.display !== 'none') hasVisible = true;
});
group.style.display = hasVisible ? '' : 'none';
});
paletteIndex = 0;
updatePaletteSelection();
}
function openPalette(initialQuery) {
var palette = document.getElementById('command-palette');
if (!palette) return;
closeAllDropdowns();
closeContextMenu();
palette.classList.add('open');
palette.setAttribute('aria-hidden', 'false');
var input = document.getElementById('command-palette-input');
if (input) {
input.value = initialQuery || '';
filterPalette(input.value);
setTimeout(function () {
input.focus();
if (initialQuery) input.select();
}, 20);
}
}
window.PolarOpenPalette = openPalette;
function activatePaletteItem(item) {
if (!item) return;
var palette = document.getElementById('command-palette');
var href = item.getAttribute('data-palette-href');
var theme = item.getAttribute('data-palette-theme');
var action = item.getAttribute('data-palette-action');
if (palette) closeDialog(palette);
if (theme) {
setTheme(theme, false);
} else if (action === 'shortcuts') {
openDialog('shortcuts-dialog');
} else if (action === 'toggle-sidebar') {
toggleSidebar();
} else if (href) {
window.location.href = href;
}
}
document.addEventListener('click', function (e) {
// Context menu action
var contextAction = e.target.closest('[data-context-action]');
if (contextAction) {
e.preventDefault();
runContextMenuAction(contextAction);
return;
}
// Context menu navigation links
if (e.target.closest('#context-menu a[href]')) {
closeContextMenu();
return;
}
if (e.target.closest('#context-menu')) return;
// Toggle sidebar
var sbBtn = e.target.closest('[data-toggle-sidebar]');
if (sbBtn) {
e.preventDefault();
toggleSidebar();
return;
}
// Dropdown trigger
var trigger = e.target.closest('[data-dropdown-trigger]');
if (trigger) {
e.stopPropagation();
var dropdown = trigger.closest('[data-dropdown]');
var menu = dropdown ? dropdown.querySelector('[data-dropdown-menu]') : null;
var wasOpen = menu && menu.classList.contains('open');
closeAllDropdowns();
if (menu && !wasOpen) {
menu.classList.add('open');
trigger.setAttribute('aria-expanded', 'true');
}
return;
}
// Theme button
var themeBtn = e.target.closest('[data-set-theme]');
if (themeBtn) {
e.preventDefault();
setTheme(themeBtn.getAttribute('data-set-theme'), false);
closeAllDropdowns();
return;
}
// Open Command Palette
var paletteTrigger = e.target.closest('[data-open-palette]');
if (paletteTrigger) {
e.preventDefault();
openPalette(paletteTrigger.getAttribute('data-open-palette') || '');
return;
}
// Palette item click
var paletteItem = e.target.closest('[data-palette-item]');
if (paletteItem) {
e.preventDefault();
activatePaletteItem(paletteItem);
return;
}
// Dialog open
var dlgOpen = e.target.closest('[data-dialog-open]');
if (dlgOpen) {
e.preventDefault();
openDialog(dlgOpen.getAttribute('data-dialog-open'));
return;
}
// Dialog close
var dlgClose = e.target.closest('[data-dialog-close]');
if (dlgClose) {
e.preventDefault();
closeDialog(dlgClose.closest('.backdrop'));
return;
}
// Backdrop click
if (e.target.classList.contains('backdrop')) {
closeDialog(e.target);
return;
}
// Mobile drawer
var drawerOpen = e.target.closest('[data-drawer-open]');
if (drawerOpen) {
var drw = document.getElementById('mobile-drawer');
if (drw) drw.classList.add('open');
return;
}
var drawerClose = e.target.closest('[data-drawer-close]');
if (drawerClose || e.target.id === 'mobile-drawer') {
var drw2 = document.getElementById('mobile-drawer');
if (drw2) drw2.classList.remove('open');
return;
}
// Copy to clipboard
var copyBtn = e.target.closest('[data-copy]');
if (copyBtn) {
e.preventDefault();
copyText(copyBtn.getAttribute('data-copy'));
return;
}
if (!e.target.closest('[data-dropdown-menu]')) {
closeAllDropdowns();
}
});
// Input handlers (palette filter + delete repo confirmation match)
document.addEventListener('input', function (e) {
if (e.target.id === 'command-palette-input') {
filterPalette(e.target.value);
return;
}
var matchExpected = e.target.getAttribute('data-confirm-input');
if (matchExpected !== null) {
var btnId = e.target.getAttribute('data-confirm-button');
var btn = btnId ? document.getElementById(btnId) : null;
if (btn) {
btn.disabled = e.target.value.trim() !== matchExpected;
}
}
if (e.target.hasAttribute('data-file-filter')) {
var q = e.target.value.trim().toLowerCase();
document.querySelectorAll('[data-tree-row]').forEach(function (row) {
var name = (row.getAttribute('data-tree-name') || '').toLowerCase();
row.style.display = !q || name.indexOf(q) !== -1 ? '' : 'none';
});
}
});
// Keyboard navigation & chords
var lastChordKey = '';
var lastChordTime = 0;
document.addEventListener('keydown', function (e) {
var palette = document.getElementById('command-palette');
var isPaletteOpen = palette && palette.classList.contains('open');
var contextMenu = document.getElementById('context-menu');
var isContextMenuOpen = contextMenu && contextMenu.classList.contains('open');
if (isContextMenuOpen) {
if (e.key === 'Escape') {
e.preventDefault();
closeContextMenu();
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
focusContextMenuItem(contextMenuIndex + 1);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
focusContextMenuItem(contextMenuIndex - 1);
return;
}
if (e.key === 'Home') {
e.preventDefault();
focusContextMenuItem(0);
return;
}
if (e.key === 'End') {
e.preventDefault();
focusContextMenuItem(getContextMenuItems().length - 1);
return;
}
if (e.key === 'Tab') {
closeContextMenu();
return;
}
if (!e.ctrlKey && !e.metaKey && !e.altKey) return;
}
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'l') {
e.preventDefault();
cycleTheme();
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
if (isPaletteOpen) {
closeDialog(palette);
} else {
openPalette('');
}
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'b') {
e.preventDefault();
toggleSidebar();
return;
}
if (e.key === 'Escape') {
closeContextMenu();
closeAllDropdowns();
document.querySelectorAll('.backdrop.open').forEach(function (b) {
closeDialog(b);
});
var drw = document.getElementById('mobile-drawer');
if (drw) drw.classList.remove('open');
return;
}
if (isPaletteOpen) {
var items = getVisiblePaletteItems();
if (e.key === 'ArrowDown') {
e.preventDefault();
paletteIndex = (paletteIndex + 1) % Math.max(1, items.length);
updatePaletteSelection();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
paletteIndex = (paletteIndex - 1 + Math.max(1, items.length)) % Math.max(1, items.length);
updatePaletteSelection();
} else if (e.key === 'Enter') {
e.preventDefault();
if (items[paletteIndex]) activatePaletteItem(items[paletteIndex]);
}
return;
}
if ((e.key === 'F10' && e.shiftKey) || e.key === 'ContextMenu') {
if (isEditableTarget(e.target)) return;
e.preventDefault();
openContextMenuForKeyboard();
return;
}
if (isEditableTarget(e.target)) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
var key = e.key.toLowerCase();
var now = Date.now();
if (lastChordKey === 'g' && now - lastChordTime < 900) {
lastChordKey = '';
lastChordTime = 0;
if (handleNavigationChord(key)) {
e.preventDefault();
return;
}
} else if (key !== 'g') {
lastChordKey = '';
}
if (key === 'g') {
lastChordKey = 'g';
lastChordTime = now;
return;
}
if (key === 'y') {
e.preventDefault();
copyText(window.location.href);
return;
}
if (e.key === '/') {
e.preventDefault();
var searchInput = document.querySelector('[data-search-input]');
if (searchInput) {
searchInput.focus();
} else {
openPalette('');
}
return;
}
if (e.key === '?') {
e.preventDefault();
openDialog('shortcuts-dialog');
return;
}
if (key === 't') {
e.preventDefault();
var fileFilter = document.querySelector('[data-file-filter]');
if (fileFilter) {
fileFilter.focus();
} else {
openPalette('file:');
}
}
});
// Avatar file picker preview & client-side 256x256 square crop/resize
document.addEventListener('change', function (e) {
if (e.target && e.target.id === 'avatar-file-input') {
var file = e.target.files && e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function (ev) {
var img = new Image();
img.onload = function () {
var size = 256;
var canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
var ctx = canvas.getContext('2d');
var minSide = Math.min(img.width, img.height);
var sx = (img.width - minSide) / 2;
var sy = (img.height - minSide) / 2;
ctx.drawImage(img, sx, sy, minSide, minSide, 0, 0, size, size);
var dataUrl = canvas.toDataURL('image/webp', 0.88);
if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) {
dataUrl = canvas.toDataURL('image/jpeg', 0.88);
}
var hidden = document.getElementById('avatar-data-input');
var rmHidden = document.getElementById('remove-avatar-input');
if (hidden) hidden.value = dataUrl;
if (rmHidden) rmHidden.value = 'false';
var previewWrap = document.getElementById('avatar-preview-wrap');
if (previewWrap) {
previewWrap.innerHTML = '
';
}
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
}
});
document.addEventListener('click', function (e) {
var rmBtn = e.target.closest('#remove-avatar-btn');
if (rmBtn) {
e.preventDefault();
var rmHidden = document.getElementById('remove-avatar-input');
var hidden = document.getElementById('avatar-data-input');
var fileInput = document.getElementById('avatar-file-input');
if (rmHidden) rmHidden.value = 'true';
if (hidden) hidden.value = '';
if (fileInput) fileInput.value = '';
var previewWrap = document.getElementById('avatar-preview-wrap');
if (previewWrap) {
previewWrap.innerHTML = '?';
}
rmBtn.style.display = 'none';
}
});
function resizeCoverImage(file, width, height, callback) {
var reader = new FileReader();
reader.onload = function (ev) {
var img = new Image();
img.onload = function () {
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
var scale = Math.max(width / img.width, height / img.height);
var sw = width / scale;
var sh = height / scale;
var sx = (img.width - sw) / 2;
var sy = (img.height - sh) / 2;
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, width, height);
var dataUrl = canvas.toDataURL('image/webp', 0.84);
if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) {
dataUrl = canvas.toDataURL('image/jpeg', 0.84);
}
callback(dataUrl);
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
}
function resizeSquareImage(file, size, callback) {
var reader = new FileReader();
reader.onload = function (ev) {
var img = new Image();
img.onload = function () {
var canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
var ctx = canvas.getContext('2d');
var minSide = Math.min(img.width, img.height);
var sx = (img.width - minSide) / 2;
var sy = (img.height - minSide) / 2;
ctx.drawImage(img, sx, sy, minSide, minSide, 0, 0, size, size);
var dataUrl = canvas.toDataURL('image/webp', 0.88);
if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) {
dataUrl = canvas.toDataURL('image/jpeg', 0.88);
}
callback(dataUrl);
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
}
document.addEventListener('change', function (e) {
if (!e.target) return;
if (e.target.id === 'background-file-input') {
var bgFile = e.target.files && e.target.files[0];
if (!bgFile) return;
resizeCoverImage(bgFile, 1600, 700, function (dataUrl) {
var hidden = document.getElementById('background-data-input');
var remove = document.getElementById('remove-background-input');
var preview = document.getElementById('background-preview');
if (hidden) hidden.value = dataUrl;
if (remove) remove.value = 'false';
if (preview) {
preview.classList.remove('is-empty');
preview.innerHTML = '
';
}
});
return;
}
if (e.target.id === 'repo-background-file-input') {
var repoBgFile = e.target.files && e.target.files[0];
if (!repoBgFile) return;
resizeCoverImage(repoBgFile, 1600, 700, function (dataUrl) {
var hidden = document.getElementById('repo-background-data-input');
var remove = document.getElementById('remove-repo-background-input');
var preview = document.getElementById('repo-background-preview');
if (hidden) hidden.value = dataUrl;
if (remove) remove.value = 'false';
if (preview) {
preview.classList.remove('is-empty');
preview.innerHTML = '
';
}
});
return;
}
if (e.target.id === 'repo-avatar-file-input') {
var repoFile = e.target.files && e.target.files[0];
if (!repoFile) return;
resizeSquareImage(repoFile, 256, function (dataUrl) {
var hidden = document.getElementById('repo-avatar-data-input');
var remove = document.getElementById('remove-repo-avatar-input');
var preview = document.getElementById('repo-avatar-preview-wrap');
if (hidden) hidden.value = dataUrl;
if (remove) remove.value = 'false';
if (preview) {
preview.innerHTML = '
';
}
});
}
});
document.addEventListener('click', function (e) {
var removeBg = e.target.closest('#remove-background-btn');
if (removeBg) {
e.preventDefault();
var hidden = document.getElementById('background-data-input');
var remove = document.getElementById('remove-background-input');
var fileInput = document.getElementById('background-file-input');
var preview = document.getElementById('background-preview');
if (hidden) hidden.value = '';
if (remove) remove.value = 'true';
if (fileInput) fileInput.value = '';
if (preview) {
preview.classList.add('is-empty');
preview.innerHTML = '';
}
removeBg.style.display = 'none';
return;
}
var removeRepoBg = e.target.closest('#remove-repo-background-btn');
if (removeRepoBg) {
e.preventDefault();
var hidden = document.getElementById('repo-background-data-input');
var remove = document.getElementById('remove-repo-background-input');
var fileInput = document.getElementById('repo-background-file-input');
var preview = document.getElementById('repo-background-preview');
if (hidden) hidden.value = '';
if (remove) remove.value = 'true';
if (fileInput) fileInput.value = '';
if (preview) {
preview.classList.add('is-empty');
preview.innerHTML = '';
}
removeRepoBg.style.display = 'none';
return;
}
var removeRepoAvatar = e.target.closest('#remove-repo-avatar-btn');
if (removeRepoAvatar) {
e.preventDefault();
var hidden = document.getElementById('repo-avatar-data-input');
var remove = document.getElementById('remove-repo-avatar-input');
var fileInput = document.getElementById('repo-avatar-file-input');
var preview = document.getElementById('repo-avatar-preview-wrap');
if (hidden) hidden.value = '';
if (remove) remove.value = 'true';
if (fileInput) fileInput.value = '';
if (preview) preview.innerHTML = '?';
removeRepoAvatar.style.display = 'none';
return;
}
var deleteUser = e.target.closest('[data-admin-delete-user]');
if (deleteUser) {
e.preventDefault();
var name = deleteUser.getAttribute('data-admin-delete-name') || '';
var userForm = document.getElementById('admin-delete-user-form');
var userInput = document.getElementById('admin_delete_confirm_user');
var userTarget = document.getElementById('admin-delete-user-target');
var userSubmit = document.getElementById('admin-delete-user-submit');
if (userForm) userForm.setAttribute('action', '/admin/users/' + deleteUser.getAttribute('data-admin-delete-user') + '/delete');
if (userTarget) userTarget.textContent = '@' + name;
if (userInput) {
userInput.value = '';
userInput.setAttribute('data-confirm-input', name);
}
if (userSubmit) userSubmit.disabled = true;
openDialog('admin-delete-user-dialog');
return;
}
var deleteRepo = e.target.closest('[data-admin-delete-repo]');
if (deleteRepo) {
e.preventDefault();
var repoName = deleteRepo.getAttribute('data-admin-delete-name') || '';
var repoForm = document.getElementById('admin-delete-repo-form');
var repoInput = document.getElementById('admin_delete_confirm_repo');
var repoTarget = document.getElementById('admin-delete-repo-target');
var repoSubmit = document.getElementById('admin-delete-repo-submit');
if (repoForm) repoForm.setAttribute('action', '/admin/repos/' + deleteRepo.getAttribute('data-admin-delete-repo') + '/delete');
if (repoTarget) repoTarget.textContent = repoName;
if (repoInput) {
repoInput.value = '';
repoInput.setAttribute('data-confirm-input', repoName);
}
if (repoSubmit) repoSubmit.disabled = true;
openDialog('admin-delete-repo-dialog');
}
});
// Repo header: the description stays on the title line and expands onto its
// own line when clicked. The avatar is sized to the title line so it always
// sits on the same row as the repository name, at a modest size.
var repoAvatarTimer = null;
function sizeRepoAvatars() {
var changed = false;
document.querySelectorAll('.repo-banner').forEach(function (banner) {
var avatar = banner.querySelector('.repo-avatar');
var title = banner.querySelector('.repo-head-row nav');
if (!avatar || !title) return;
var h = Math.round(title.getBoundingClientRect().height);
if (h <= 0) return;
var size = Math.min(30, Math.max(18, h));
var sizePx = size + 'px';
if (avatar.style.width === sizePx) return;
avatar.style.width = sizePx;
avatar.style.height = sizePx;
avatar.style.fontSize = Math.min(20, Math.max(9, Math.round(size * 0.42))) + 'px';
changed = true;
});
return changed;
}
function updateRepoDescs() {
document.querySelectorAll('[data-repo-desc]').forEach(function (el) {
var banner = el.closest('.repo-banner');
if (banner && banner.classList.contains('desc-open')) {
el.classList.add('truncated');
return;
}
el.classList.toggle('truncated', el.scrollWidth > el.clientWidth + 1);
});
if (sizeRepoAvatars()) {
// Re-measure once the width transition settles: the final avatar width
// changes the room left for the description.
window.clearTimeout(repoAvatarTimer);
repoAvatarTimer = window.setTimeout(sizeRepoAvatars, 300);
}
}
document.addEventListener('click', function (e) {
var desc = e.target.closest('[data-repo-desc]');
if (!desc || !desc.classList.contains('truncated')) return;
var banner = desc.closest('.repo-banner');
if (!banner) return;
banner.classList.toggle('desc-open');
updateRepoDescs();
});
window.addEventListener('resize', updateRepoDescs);
document.addEventListener('htmx:afterSwap', updateRepoDescs);
updateRepoDescs();
})();