Files
GitHop/deploy/gitea/public/assets/js/hop-native.js
T
Hop ae2ffae36d Add Hop logo and an explanatory public landing page
Hop-State: A_06FN4GBGQFKPXH5B5MK4P0R
Hop-Proposal: R_06FN4GAZCEER9W65XJTCVJG
Hop-Task: T_06FN4FT3436TX5AV35QM4RR
Hop-Attempt: AT_06FN4FT3401X9BHCJVKT52G
2026-07-11 10:37:28 -07:00

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.svg', '/assets/img/hop.svg');
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});
})();