2f535ffb9d
Hop-State: A_06FN4JF3RBE9MTQ85C2GNX0 Hop-Proposal: R_06FN4JDDC8C91T769MY7WR8 Hop-Task: T_06FN4JACX74MAJEYJ24HE7G Hop-Attempt: AT_06FN4JACX50N1VPWMKJ0H1G
168 lines
5.4 KiB
JavaScript
168 lines
5.4 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
const exactLabels = new Map([
|
|
['Code', 'Files'],
|
|
['Issues', 'Tasks'],
|
|
['Pull Requests', 'Proposals'],
|
|
['Pull requests', 'Proposals'],
|
|
['Actions', 'Evidence'],
|
|
['Commits', 'Checkpoints'],
|
|
['Branches', 'Attempts'],
|
|
['New Issue', 'New Task'],
|
|
['Create Issue', 'Create Task'],
|
|
['New Pull Request', 'New Proposal'],
|
|
['Create Pull Request', 'Create Proposal'],
|
|
['Merge Pull Request', 'Accept Proposal'],
|
|
['Compare & pull request', 'Review as Proposal'],
|
|
]);
|
|
|
|
const phraseLabels = [
|
|
[/\bPull Requests\b/g, 'Proposals'],
|
|
[/\bPull requests\b/g, 'Proposals'],
|
|
[/\bpull requests\b/g, 'proposals'],
|
|
[/\bNew Issue\b/g, 'New Task'],
|
|
[/\bnew issue\b/g, 'new task'],
|
|
[/\bIssues\b/g, 'Tasks'],
|
|
[/\bissues\b/g, 'tasks'],
|
|
[/\bActions\b/g, 'Evidence'],
|
|
[/\bCommits\b/g, 'Checkpoints'],
|
|
[/\bBranches\b/g, 'Attempts'],
|
|
];
|
|
|
|
const semanticRoots = [
|
|
'#navbar',
|
|
'.secondary-nav',
|
|
'[role="main"] .ui.header',
|
|
'[role="main"] h1',
|
|
'[role="main"] h2',
|
|
'[role="main"] h3',
|
|
'[role="main"] .ui.button',
|
|
'[role="main"] .menu',
|
|
'[role="main"] .breadcrumb',
|
|
'[role="main"] label',
|
|
].join(',');
|
|
|
|
function replaceExactText(root) {
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
const nodes = [];
|
|
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
|
|
for (const node of nodes) {
|
|
const original = node.nodeValue;
|
|
const trimmed = original.trim();
|
|
const replacement = exactLabels.get(trimmed);
|
|
if (!replacement) continue;
|
|
node.nodeValue = original.replace(trimmed, replacement);
|
|
}
|
|
}
|
|
|
|
function replacePhrases(value) {
|
|
let result = value;
|
|
for (const [pattern, replacement] of phraseLabels) {
|
|
result = result.replace(pattern, replacement);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function replaceAccessibleLabels(root) {
|
|
const attributes = ['aria-label', 'data-tooltip-content', 'placeholder', 'title'];
|
|
for (const element of root.querySelectorAll('[aria-label], [data-tooltip-content], [placeholder], [title]')) {
|
|
for (const attribute of attributes) {
|
|
const value = element.getAttribute(attribute);
|
|
if (!value) continue;
|
|
const replacement = replacePhrases(value);
|
|
if (replacement !== value) element.setAttribute(attribute, replacement);
|
|
}
|
|
}
|
|
}
|
|
|
|
function repoNavigation() {
|
|
return document.querySelector('.secondary-nav .overflow-menu-items');
|
|
}
|
|
|
|
function navigationLink(suffix) {
|
|
const navigation = repoNavigation();
|
|
if (!navigation) return null;
|
|
return [...navigation.querySelectorAll(':scope > a.item')].find((link) => {
|
|
const path = new URL(link.href, window.location.origin).pathname.replace(/\/$/, '');
|
|
return path.endsWith(suffix);
|
|
}) || null;
|
|
}
|
|
|
|
function workflowLink(source, label) {
|
|
if (!source) return null;
|
|
const link = document.createElement('a');
|
|
link.className = 'flex-text-block muted';
|
|
link.href = source.href;
|
|
link.setAttribute('aria-label', label);
|
|
|
|
const icon = source.querySelector('svg');
|
|
if (icon) link.append(icon.cloneNode(true));
|
|
link.append(document.createTextNode(` ${label}`));
|
|
|
|
const count = source.querySelector('.ui.label');
|
|
if (count) link.append(count.cloneNode(true));
|
|
return link;
|
|
}
|
|
|
|
function addWorkflowSummary() {
|
|
const sidebar = document.querySelector('.repo-home-sidebar-top');
|
|
if (!sidebar || sidebar.querySelector('.hop-native-workflow')) return;
|
|
|
|
const links = [
|
|
workflowLink(navigationLink('/issues'), 'Tasks'),
|
|
workflowLink(navigationLink('/pulls'), 'Proposals'),
|
|
workflowLink(navigationLink('/actions'), 'Evidence'),
|
|
].filter(Boolean);
|
|
if (!links.length) return;
|
|
|
|
const list = document.createElement('div');
|
|
list.className = 'flex-list hop-native-workflow';
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'flex-item';
|
|
const main = document.createElement('div');
|
|
main.className = 'flex-item-main';
|
|
const title = document.createElement('div');
|
|
title.className = 'flex-item-title';
|
|
title.textContent = 'Hop workflow';
|
|
const body = document.createElement('div');
|
|
body.className = 'flex-item-body tw-text-15';
|
|
const linkList = document.createElement('div');
|
|
linkList.className = 'tw-flex tw-flex-col tw-gap-2 tw-mt-2';
|
|
for (const link of links) linkList.append(link);
|
|
|
|
body.append(linkList);
|
|
main.append(title, body);
|
|
item.append(main);
|
|
list.append(item);
|
|
sidebar.prepend(list);
|
|
}
|
|
|
|
function applyHopSemantics() {
|
|
const brandLogo = document.querySelector('#navbar-logo img');
|
|
if (brandLogo) brandLogo.src = brandLogo.src.replace(/\/img\/(?:logo|hop)\.svg(?:\?.*)?$/, '/img/hop.svg?v=2');
|
|
for (const root of document.querySelectorAll(semanticRoots)) replaceExactText(root);
|
|
replaceAccessibleLabels(document);
|
|
addWorkflowSummary();
|
|
const title = replacePhrases(document.title);
|
|
if (title !== document.title) document.title = title;
|
|
document.documentElement.dataset.hopNative = 'true';
|
|
}
|
|
|
|
let queued = false;
|
|
function queueApply() {
|
|
if (queued) return;
|
|
queued = true;
|
|
window.requestAnimationFrame(() => {
|
|
queued = false;
|
|
applyHopSemantics();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', queueApply);
|
|
document.addEventListener('htmx:afterSwap', queueApply);
|
|
new MutationObserver(queueApply).observe(document.documentElement, {childList: true, subtree: true});
|
|
})();
|