Add a no-redesign semantic layer that makes native Gitea collaboration Hop-first
Hop-State: A_06FN45YJ09WC2QFGJM27VE8 Hop-Proposal: R_06FN45WVVTHC245S2BK503G Hop-Task: T_06FN3MVGY3MT82ESQ89BND0 Hop-Attempt: AT_06FN3MVGY092BFA5MR9C7EG
This commit is contained in:
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
env_file=${ENV_FILE:-.env}
|
||||
|
||||
if [ ! -f "$env_file" ]; then
|
||||
echo "missing $env_file; copy .env.example and configure it first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose --env-file "$env_file" up -d gitea
|
||||
docker compose --env-file "$env_file" exec -T -u root gitea \
|
||||
mkdir -p /data/gitea/templates/custom /data/gitea/public/assets/js
|
||||
docker compose --env-file "$env_file" cp \
|
||||
deploy/gitea/templates/custom/footer.tmpl \
|
||||
gitea:/data/gitea/templates/custom/footer.tmpl
|
||||
docker compose --env-file "$env_file" cp \
|
||||
deploy/gitea/public/assets/js/hop-native.js \
|
||||
gitea:/data/gitea/public/assets/js/hop-native.js
|
||||
docker compose --env-file "$env_file" exec -T -u root gitea \
|
||||
chown -R git:git /data/gitea/templates /data/gitea/public
|
||||
docker compose --env-file "$env_file" exec -T -u git \
|
||||
-e GITEA____APP_NAME=Hop gitea \
|
||||
gitea --config /data/gitea/conf/app.ini config edit-ini --in-place --apply-env
|
||||
docker compose --env-file "$env_file" restart gitea
|
||||
|
||||
attempt=0
|
||||
until curl --fail --silent http://localhost:3000/api/healthz >/dev/null; do
|
||||
attempt=$((attempt + 1))
|
||||
if [ "$attempt" -ge 30 ]; then
|
||||
echo "Gitea did not become healthy" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Hop-native Gitea semantics installed at http://localhost:3000"
|
||||
@@ -0,0 +1,165 @@
|
||||
(() => {
|
||||
'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() {
|
||||
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});
|
||||
})();
|
||||
@@ -0,0 +1,2 @@
|
||||
<script src="{{AssetUrlPrefix}}/js/hop-native.js?v=1"></script>
|
||||
|
||||
Reference in New Issue
Block a user