Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474{{ define "repo/fragments/diff" }} <style> #filesToggle:checked ~ div label[for="filesToggle"] .show-text { display: none; } #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; } #filesToggle:not(:checked) ~ div div#files { width: 0; display: none; margin-right: 0; } #filesToggle:not(:checked) ~ div div#resize-files { display: none; } </style>
<div id="diff-area"> {{ template "diffTopbar" . }} {{ block "diffLayout" . }} {{ end }} {{ template "fragments/resizable" }} {{ template "activeFileHighlight" }} {{ template "fragments/line-quote-button" }} {{ template "reviewState" }} </div>{{ end }}
{{ define "diffTopbar" }} {{ $diff := index . 0 }} {{ $opts := index . 1 }} {{ $root := "" }} {{ if gt (len .) 2 }} {{ $root = index . 2 }} {{ end }}
{{ block "filesCheckbox" $ }} {{ end }} {{ block "subsCheckbox" $ }} {{ end }}
<!-- top bar --> <div class="sticky top-0 z-10 bg-slate-100 dark:bg-gray-900 flex items-center gap-2 col-span-full h-12 p-2 {{ if $root }}mt-4{{ end }}"> <!-- left panel toggle --> {{ template "filesToggle" . }}
<!-- stats --> {{ $stat := $diff.Stats }} {{ $count := len $diff.ChangedFiles }} {{ template "repo/fragments/diffStatPill" $stat }} <span id="changed-files-label" class="text-xs text-gray-600 dark:text-gray-400 hidden md:inline-flex" data-total="{{ $count }}">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span>
{{ if $root }} {{ if $root.IsInterdiff }} <!-- interdiff indicator --> <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> <span class="text-xs text-gray-600 dark:text-gray-400 tracking-wide">Interdiff</span> <a href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ sub $root.ActiveRound 1 }}" class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" > #{{ sub $root.ActiveRound 1 }} </a> <span class="text-gray-400 text-xs">→</span> <a href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $root.ActiveRound }}" class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" > #{{ $root.ActiveRound }} </a> </div> {{ else if ne $root.ActiveRound nil }} <!-- diff round indicator --> <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> <span class="text-xs text-gray-600 dark:text-gray-400 tracking-wide">Diff</span> <span class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs border border-gray-300 dark:border-gray-600"> <span class="hidden md:inline">Round </span>#{{ $root.ActiveRound }} </span> </div> {{ end }} {{ end }}
<!-- spacer --> <div class="flex-grow"></div>
<!-- collapse diffs --> {{ template "collapseToggle" }}
<!-- diff options --> {{ template "repo/fragments/diffOpts" $opts }}
<!-- right panel toggle --> {{ block "subsToggle" $ }} {{ end }} </div>
{{ end }}
{{ define "resize-grip" }} {{ $id := index . 0 }} {{ $target := index . 1 }} {{ $direction := index . 2 }} <div id="{{ $id }}" data-resizer="vertical" data-target="{{ $target }}" data-direction="{{ $direction }}" class="resizer-vertical hidden md:flex w-4 sticky top-12 max-h-screen flex-col items-center justify-center group"> <div class="w-1 h-16 group-hover:h-24 group-[.resizing]:h-24 transition-all rounded-full bg-gray-400 dark:bg-gray-500 group-hover:bg-gray-500 group-hover:dark:bg-gray-400"></div> </div>{{ end }}
{{ define "diffLayout" }} {{ $diff := index . 0 }} {{ $opts := index . 1 }} {{ $padBottom := and (gt (len .) 2) (index . 2) }} {{ $blobBase := "" }} {{ if gt (len .) 3 }}{{ $blobBase = index . 3 }}{{ end }}
<div class="flex col-span-full flex-grow"> <!-- left panel --> <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto {{ if $padBottom }}pb-12{{ end }}"> <section class="overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded bg-white dark:bg-gray-800 drop-shadow-sm"> {{ template "repo/fragments/fileTree" $diff.FileTree }} </section> </div>
{{ template "resize-grip" (list "resize-files" "files" "before") }}
<!-- main content --> <div id="diff-files" class="flex-1 min-w-0 sticky top-12 {{ if $padBottom }}pb-12{{ end }}"> {{ template "diffFiles" (list $diff $opts $blobBase) }} </div>
</div>{{ end }}
{{ define "diffFiles" }} {{ $diff := index . 0 }} {{ $opts := index . 1 }} {{ $blobBase := "" }} {{ if gt (len .) 2 }}{{ $blobBase = index . 2 }}{{ end }} {{ $files := $diff.ChangedFiles }} {{ $isSplit := $opts.Split }} <div class="flex flex-col gap-4"> {{ if eq (len $files) 0 }} <div class="text-center text-gray-500 dark:text-gray-400 py-8"> <p>No differences found between the selected revisions.</p> </div> {{ else }} {{ range $idx, $file := $files }} {{ template "diffFile" (list $idx $file $isSplit false $blobBase) }} {{ end }} {{ end }} </div>{{ end }}
{{ define "diffFile" }} {{ $idx := index . 0 }} {{ $file := index . 1 }} {{ $isSplit := index . 2 }} {{ $collapsed := false }} {{ if gt (len .) 3 }}{{ $collapsed = index . 3 }}{{ end }} {{ $blobBase := "" }} {{ if gt (len .) 4 }}{{ $blobBase = index . 4 }}{{ end }} {{ $isGenerated := false }} {{ $isDeleted := false }} {{ with $file }} {{ $n := .Names }} {{ $isDeleted = and (eq $n.New "") (ne $n.Old "") }} {{ if $n.New }} {{ $isGenerated = isGenerated $n.New }} {{ else if $n.Old }} {{ $isGenerated = isGenerated $n.Old }} {{ end }} <details {{ if and (not $isGenerated) (not $isDeleted) (not $collapsed) }}open{{ end }} id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> <summary class="list-none cursor-pointer {{ if not $collapsed }}sticky top-12{{ end }} group-open:border-b border-gray-200 dark:border-gray-700"> <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> <span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span> <span class="hidden group-open:inline">{{ i "chevron-down" "w-4 h-4" }}</span> {{ template "repo/fragments/diffStatPill" .Stats }}
<div class="flex gap-2 items-center overflow-x-auto"> {{ if and $n.New $n.Old (ne $n.New $n.Old)}} {{ $n.Old }} {{ i "arrow-right" "w-4 h-4" }} {{ $n.New }} {{ else if $n.New }} {{ $n.New }} {{ else }} {{ $n.Old }} {{ end }} {{ if $isDeleted }} <span class="text-gray-400 dark:text-gray-500" title="Deleted files are collapsed by default"> {{ i "circle-question-mark" "size-4" }} </span> {{ else if $isGenerated }} <span class="text-gray-400 dark:text-gray-500" title="Generated files are collapsed by default"> {{ i "circle-question-mark" "size-4" }} </span> {{ end }} </div> </div> <div class="flex items-center pr-1"> {{ if and $blobBase $n.New }} <a href="{{ $blobBase }}/{{ $n.New }}" onclick="event.stopPropagation()" class="btn-flat hidden md:flex" title="Open file" > {{ i "eye" "size-4" }} <span class="hidden md:inline">View file</span> </a> {{ end }} <label data-review-btn="file-{{ .Id }}" onclick="event.stopPropagation()" class="review-btn hidden p-2 items-center gap-1 text-xs text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 transition-colors cursor-pointer" title="Mark as reviewed" > <input type="checkbox" class="sr-only peer review-checkbox" data-file-id="file-{{ .Id }}" /> <span class="peer-checked:hidden">{{ i "circle" "size-4" }}</span> <span class="hidden peer-checked:inline text-green-600 dark:text-green-400">{{ i "circle-check" "size-4" }}</span> <span class="hidden md:inline">Reviewed</span> </label> </div> </div> </summary>
<div class="transition-all duration-700 ease-in-out"> {{ $reason := .CanRender }} {{ if $reason }} <p class="text-center text-gray-400 dark:text-gray-500 p-4">{{ $reason }}</p> {{ else }} {{ if $isSplit }} {{- template "repo/fragments/splitDiff" .Split -}} {{ else }} {{- template "repo/fragments/unifiedDiff" . -}} {{ end }} {{- end -}} </div> </details> {{ end }}{{ end }}
{{ define "filesCheckbox" }} <input type="checkbox" id="filesToggle" class="peer/files hidden" {{ if gt (len .) 2 }}checked{{ end }}/>{{ end }}
{{ define "filesToggle" }} <label title="Toggle filetree panel" for="filesToggle" class="hidden md:inline-flex items-center justify-center rounded cursor-pointer text-normal font-normal normalcase"> <span class="show-text">{{ i "panel-left-open" "size-4" }}</span> <span class="hide-text">{{ i "panel-left-close" "size-4" }}</span> </label>{{ end }}
{{ define "collapseToggle" }} <label title="Expand/Collapse diffs" for="collapseToggle" class="btn font-normal normal-case" > <input type="checkbox" id="collapseToggle" class="peer/collapse hidden" checked/> <span class="peer-checked/collapse:hidden inline-flex items-center gap-2"> {{ i "unfold-vertical" "w-4 h-4" }} <span class="hidden md:inline">Expand all</span> </span> <span class="peer-checked/collapse:inline-flex hidden flex items-center gap-2"> {{ i "fold-vertical" "w-4 h-4" }} <span class="hidden md:inline">Collapse all</span> </span> </label> <script> (() => { const checkbox = document.getElementById('collapseToggle'); const diffArea = document.getElementById('diff-area');
checkbox.addEventListener('change', () => { document.querySelectorAll('details[id^="file-"]').forEach(detail => { detail.open = checkbox.checked; }); });
if (window.__collapseToggleHandler) { diffArea.removeEventListener('toggle', window.__collapseToggleHandler, true); }
const handler = (e) => { if (!e.target.matches('details[id^="file-"]')) return; const details = document.querySelectorAll('details[id^="file-"]'); const allOpen = Array.from(details).every(d => d.open); const allClosed = Array.from(details).every(d => !d.open);
if (allOpen) checkbox.checked = true; else if (allClosed) checkbox.checked = false; };
window.__collapseToggleHandler = handler; diffArea.addEventListener('toggle', handler, true); })(); </script>{{ end }}
{{ define "activeFileHighlight" }} <script> (() => { if (window.__activeFileScrollHandler) { document.removeEventListener('scroll', window.__activeFileScrollHandler); }
const filetreeLinks = document.querySelectorAll('.filetree-link'); if (filetreeLinks.length === 0) return;
const linkMap = new Map(); filetreeLinks.forEach(link => { const path = link.getAttribute('data-path'); if (path) linkMap.set('file-' + path, link); });
let currentActive = null; const setActive = (link) => { if (link && link !== currentActive) { if (currentActive) currentActive.classList.remove('font-bold'); link.classList.add('font-bold'); currentActive = link; } };
filetreeLinks.forEach(link => { link.addEventListener('click', () => setActive(link)); });
const topbar = document.querySelector('.sticky.top-0.z-30'); const headerHeight = topbar ? topbar.offsetHeight : 0;
const updateActiveFile = () => { const diffFiles = document.querySelectorAll('details[id^="file-"]'); Array.from(diffFiles).some(file => { const rect = file.getBoundingClientRect(); if (rect.top <= headerHeight && rect.bottom > headerHeight) { setActive(linkMap.get(file.id)); return true; } return false; }); };
window.__activeFileScrollHandler = updateActiveFile; document.addEventListener('scroll', updateActiveFile); updateActiveFile(); })(); </script>{{ end }}
{{ define "reviewState" }} <script> (() => { const linkBase = document.getElementById('round-link-base'); if (!linkBase) return;
const isInterdiff = !!document.getElementById('is-interdiff'); const basePath = linkBase.value.replace(/^\//, ''); const storageKey = 'reviewed:' + basePath + (isInterdiff ? '/interdiff' : '');
const REVIEWED_PREFIX = 'reviewed:'; const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const load = () => { try { const entry = JSON.parse(localStorage.getItem(storageKey) || '{}'); return new Set(Array.isArray(entry) ? entry : (entry.files || [])); } catch { return new Set(); } };
const save = (reviewed) => { const liveIds = new Set(Array.from(allFiles()).map(d => d.id)); localStorage.setItem(storageKey, JSON.stringify({ files: Array.from(reviewed).filter(id => liveIds.has(id)), ts: Date.now(), })); };
const pruneStale = () => { const now = Date.now(); Object.keys(localStorage) .filter(k => k.startsWith(REVIEWED_PREFIX) && k !== storageKey) .forEach(k => { try { const entry = JSON.parse(localStorage.getItem(k)); if (!entry.ts || now - entry.ts > MAX_AGE_MS) localStorage.removeItem(k); } catch { localStorage.removeItem(k); } }); }; if (Math.random() < 0.1) pruneStale();
const allFiles = () => document.querySelectorAll('details[id^="file-"]');
const applyOne = (fileId, isReviewed) => { const detail = document.getElementById(fileId); if (!detail) return;
const btn = detail.querySelector('[data-review-btn]'); const checkbox = btn?.querySelector('input[type="checkbox"]'); const path = CSS.escape(fileId.replace('file-', '')); const treeLink = document.querySelector(`.filetree-link[data-path="${path}"]`);
detail.classList.toggle('opacity-60', isReviewed);
if (checkbox) checkbox.checked = isReviewed;
if (treeLink) { const existing = treeLink.parentElement.querySelector('.review-indicator'); if (isReviewed && !existing) { const indicator = document.createElement('span'); indicator.className = 'review-indicator text-green-600 dark:text-green-400 flex-shrink-0'; indicator.innerHTML = '✓'; treeLink.parentElement.appendChild(indicator); } else if (!isReviewed && existing) { existing.remove(); } } };
const updateProgress = (reviewed) => { const el = document.getElementById('changed-files-label'); if (!el) return; const total = parseInt(el.dataset.total, 10); const files = allFiles(); const count = Array.from(files).filter(d => reviewed.has(d.id)).length; const suffix = total === 1 ? 'file' : 'files'; const allDone = count === total; el.classList.toggle('text-green-600', allDone); el.classList.toggle('dark:text-green-400', allDone); el.classList.toggle('text-gray-600', !allDone); el.classList.toggle('dark:text-gray-400', !allDone); el.textContent = count > 0 ? `${count}/${total} ${suffix} reviewed` : `${total} changed ${suffix}`; };
const reviewed = load();
const toggleReview = (fileId) => { const detail = document.getElementById(fileId); if (!detail) return; const isNowReviewed = !reviewed.has(fileId); if (isNowReviewed) { reviewed.add(fileId); detail.open = false; } else { reviewed.delete(fileId); } save(reviewed); applyOne(fileId, isNowReviewed); updateProgress(reviewed); };
document.getElementById('diff-area').addEventListener('change', (e) => { const checkbox = e.target.closest('.review-checkbox'); if (!checkbox) return; const fileId = checkbox.dataset.fileId; if (fileId) toggleReview(fileId); });
document.querySelectorAll('.review-btn').forEach(btn => { btn.classList.remove('hidden'); btn.classList.add('flex'); });
allFiles().forEach(detail => { if (reviewed.has(detail.id)) { applyOne(detail.id, true); detail.open = false; } }); updateProgress(reviewed); })(); </script>{{ end }}