Tucker McKnight <tucker@pangolin.lan> | Mon Dec 29 2025
Add clone buttons and homepage buttons to index Fix some Js errors and make buttons use classes instead of IDs since there are multiple of them on the index page. Some style changes.
2 3 4 5 6 7
const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme
setCheckbox(currentTheme, document.getElementById('dark-mode-switch'))
const copyCommand = (event) => {
const elem = event.target2 3 4 5 6 7 8 9
const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme
if (setCheckbox) {
setCheckbox(currentTheme, document.getElementById('dark-mode-switch'))
}
const copyCommand = (event) => {
const elem = event.target17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
}, 5000)
}
const createClonePopover = () => {
const div = document.createElement('div')
div.id = "clone-popover"
div.innerHTML = `
<label class='form-label'>HTTPS URL</label>
<div class='input-group d-flex flex-nowrap'>
<span class='clone overflow-hidden input-group-text'>${window.cloneUrl}</span>
<button data-copy-text='${window.cloneUrl}' class='btn btn-primary shadow-none text-white' id='copy-button'>Copy</button>
</div>`
div.querySelector("#copy-button").addEventListener('click', copyCommand)
const popoverBtn = document.getElementById("clone-popover-btn")
const bsPopover = new bootstrap.Popover(popoverBtn, {
sanitize: false,
html: true,17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
}, 5000)
}
const createClonePopover = (popoverBtn, url) => {
const div = document.createElement('div')
// div.id = "clone-popover"
div.innerHTML = `
<label class='form-label'>HTTPS URL</label>
<div class='input-group d-flex flex-nowrap'>
<span class='clone overflow-hidden input-group-text'>${url || window.cloneUrl}</span>
<button data-copy-text='${url || window.cloneUrl}' class='btn btn-primary shadow-none text-white' id='copy-button'>Copy</button>
</div>`
div.querySelector("#copy-button").addEventListener('click', copyCommand)
// const popoverBtn = document.querySelector(".clone-popover-btn")
const bsPopover = new bootstrap.Popover(popoverBtn, {
sanitize: false,
html: true,39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
container: 'body',
})
document.body.addEventListener('click', (event) => {
const target = event.target
// If they didn't click the #clone-popover-btn or if we're not inside of
// popover, or if we *are* inside of a popover but a different one than the
// current one, then close the popover.
const parentPopover = target.closest(".popover")
if (
target.id !== "clone-popover-btn"
&& (
parentPopover === null
|| parentPopover !== bsPopover.tip)
) {
bsPopover.hide()
}
})
}
const toggleLastTouch = (event) => {39 40 41 42 43 44
container: 'body',
})
window.popovers.push(bsPopover)
}
const toggleLastTouch = (event) => {98 99 100 101 102 103 104 105
const bootstrap = window.bootstrap
const jsVars = window.jsVars
if (document.getElementById('clone-popover-btn')) {
createClonePopover()
}
document.querySelector("#copy-button").addEventListener('click', copyCommand)
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
const bootstrap = window.bootstrap
const jsVars = window.jsVars
window.popovers ||= []
document.querySelectorAll('.clone-popover-btn').forEach((button) => {
createClonePopover(button, button.dataset['copyText'])
})
document.querySelectorAll(".copy-button").forEach((button) => {
button.addEventListener('click', copyCommand)
})
document.addEventListener('click', (event) => {
const target = event.target
// If they didn't click the .clone-popover-btn or if we're not inside of
// popover, or if we *are* inside of a popover but a different one than the
// current one, then close the popover.
const parentPopover = target.closest(".popover")
if (
!target.classList.contains("clone-popover-btn")
&& parentPopover === null
) {
window.popovers.forEach(popover => popover.hide())
}
})12 13 14 15 16 17
<h1 class="fs-2">${data.patchInfo.commit.message.split('\n')[0]}</h2>
<div class="input-group mb-2">
<span class="font-monospace input-group-text border-info text-white text-bg-dark">${data.patchInfo.commit.hash}</span>
<button data-copy-text='${data.patchInfo.commit.hash}' class="btn btn-info shadow-none" id="copy-button">Copy</button>
</div>
<p>${data.patchInfo.commit.author} | ${date(data.patchInfo.commit.date)}</p>
<pre class="mb-0">${data.patchInfo.commit.message}</pre>12 13 14 15 16 17
<h1 class="fs-2">${data.patchInfo.commit.message.split('\n')[0]}</h2>
<div class="input-group mb-2">
<span class="font-monospace input-group-text border-info text-white text-bg-dark">${data.patchInfo.commit.hash}</span>
<button data-copy-text='${data.patchInfo.commit.hash}' class="btn btn-info shadow-none copy-button">Copy</button>
</div>
<p>${data.patchInfo.commit.author} | ${date(data.patchInfo.commit.date)}</p>
<pre class="mb-0">${data.patchInfo.commit.message}</pre>39 40 41 42 43 44
${data.patchInfo.commit.diffs.map((hunk) => {
return `
<div class=hunk>
<span class="font-monospace fw-bold"><a href="${data.reposPath}/${slugify(data.patchInfo.repoName)}/branches/${slugify(data.patchInfo.branchName)}/files/${slugify(hunk.fileName)}.html">${hunk.fileName}:${hunk.lineNumber}</a></span>
<div class="diff d-flex">
<div class="flex-grow-1 diff-left pe-2">
<span class='font-monospace text-secondary'>Before</span>39 40 41 42 43 44
${data.patchInfo.commit.diffs.map((hunk) => {
return `
<div class=hunk>
<span class="font-monospace fw-bold"><a href="${data.reposPath}/${slugify(data.patchInfo.repoName)}/branches/${slugify(data.patchInfo.branchName)}/files/${hunk.fileName.split('/').map((filePart) => slugify(filePart)).join('/')}">${hunk.fileName}:${hunk.lineNumber}</a></span>
<div class="diff d-flex">
<div class="flex-grow-1 diff-left pe-2">
<span class='font-monospace text-secondary'>Before</span>0 1 2 3 4
export default (branches: Array<{name: string, href: string, date: string}>, defaultBranch: string, currentBranch: string): string => {
return branches.map((branch) => {
const currentBadge = currentBranch === branch.name ? '<div class="badge rounded-pill bg-primary mx-1">current</div>' : ''
const defaultBadge = defaultBranch === branch.name ? '<div class="badge rounded-pill bg-info text-dark mx-1">default</div>' : ''
return `<a href='${branch.href}' class='dropdown-item my-1'><span class="branch-dropdown-branch-name me-1">${branch.name}</span>${currentBadge}${defaultBadge}<span class="text-body d-block ms-2">updated ${branch.date}</span></a>`0 1 2 3 4
export default (branches: Array<{name: string, href: string, date: string}>, defaultBranch: string, currentBranch: string): string => {
return branches.map((branch) => {
const currentBadge = currentBranch === branch.name ? '<div class="badge rounded-pill bg-secondary mx-1">current</div>' : ''
const defaultBadge = defaultBranch === branch.name ? '<div class="badge rounded-pill bg-info text-dark mx-1">default</div>' : ''
return `<a href='${branch.href}' class='dropdown-item my-1'><span class="branch-dropdown-branch-name me-1">${branch.name}</span>${currentBadge}${defaultBadge}<span class="text-body d-block ms-2">updated ${branch.date}</span></a>`111 112 113 114 115 116
<nav class="navbar navbar-expand">
<ul class="main-nav navbar-nav flex-wrap">
<li class="nav-item">
<a class="nav-link ${data.navTab === 'home' ? 'active' : ''}" aria-current="page" href="${nav.repoCurrentBranchHome()}">ReadMe</a>
</li>
<li class="nav-item">
<a class="nav-link ${data.navTab === 'files' ? 'active' : ''}" href="${nav.repoCurrentBranchFiles()}">Files</a>111 112 113 114 115 116
<nav class="navbar navbar-expand">
<ul class="main-nav navbar-nav flex-wrap">
<li class="nav-item">
<a class="nav-link ${data.navTab === 'home' ? 'active' : ''}" aria-current="page" href="${nav.repoCurrentBranchHome()}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link ${data.navTab === 'files' ? 'active' : ''}" href="${nav.repoCurrentBranchFiles()}">Files</a>13 14 15 16 17 18
<div class="row my-3">
<div class="col">
<h3>
<span class="bezel-gray px-1"><a href="${data.reposPath}/${data.fileInfo.repoName}/branches/${data.fileInfo.branchName}/files">🏠 ./</a></span>${
data.fileInfo.file.split('/').map((dir, index, arr) => {
if (index === arr.length - 1) {
return `<span class="px-2">${dir}</span>`13 14 15 16 17 18
<div class="row my-3">
<div class="col">
<h3>
<span class="bezel-gray px-1"><a href="${data.reposPath}/${data.fileInfo.repoName}/branches/${data.fileInfo.branchName}/files">./</a></span>${
data.fileInfo.file.split('/').map((dir, index, arr) => {
if (index === arr.length - 1) {
return `<span class="px-2">${dir}</span>`70 71 72 73 74 75
data.fileInfo.file
)).map(
(annotation) => {
return `<a href="${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/patches/${annotation.sha}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
}
).join('\n')
}</pre></code>70 71 72 73 74 75
data.fileInfo.file
)).map(
(annotation) => {
return `<a href="${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/commits/${annotation.sha}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
}
).join('\n')
}</pre></code>10 11 12 13 14 15
<div class="row my-3">
<div class="col">
<h3>
<span>🏠 ./</span>
</h3>
</div>
</div>10 11 12 13 14 15
<div class="row my-3">
<div class="col">
<h3>
<span>./</span>
</h3>
</div>
</div>14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
<div class="container my-4">
<div class="row">
<div class="col">
<h1>Tucker's Repositories</h1>
</div>
</div>
<div class="row">
<div class="col d-flex flex-wrap">
${data.repos.map((repo) => {
return `
<div class="mx-2 card bezel-gray" style="width: 18rem;">
<div class="card-body">
<h2 class="card-title fs-5">${repo.name}</h2>
${repo.description ? `<p class="card-text">${repo.description}</p>` : ''}
<a href="${data.reposPath}/${slugify(repo.name)}/branches/${repo.defaultBranch}" class="btn btn-primary text-white">Go to ${repo.name}</a>
</div>
</div>
`14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
<div class="container my-4">
<div class="row">
<div class="col">
<h1>${data.reposConfig.defaultTemplateConfiguration?.allRepositoriesPageTitle || "All Repositories"}</h1>
</div>
</div>
<div class="row">
<div class="col d-flex flex-wrap">
${data.repos.map((repo) => {
return `
<div class="m-2 card bezel-gray flex-grow-1" style="flex-basis: 20rem;">
<div class="card-header">
<a class="card-title fs-5" href="${data.reposPath}/${slugify(repo.name)}/branches/${repo.defaultBranch}">${repo.name}</a>
</div>
<div class="card-body">
${repo.description ? `<p class="card-text">${repo.description}</p>` : ''}
</div>
<div class="card-footer">
<a href="${data.reposPath}/${slugify(repo.name)}/branches/${repo.defaultBranch}" class="mx-1 my-2 btn btn-primary text-white">Go to site</a>
<button class="mx-1 my-2 btn btn-outline-secondary shadow-none dropdown-toggle clone-popover-btn" data-copy-text="${repo.cloneUrl}">Clone</button>
${(data.reposConfig.repos[repo.name].defaultTemplateConfiguration?.homepageButtons || []).map((button) => {
return `<a class="mx-1 my-2 btn btn-outline-secondary shadow-none" href="${button.url}" ${button.newTab ? 'target="_blank"' : ''}>${button.text}${button.newTab ? ' <span>⧉</span>' : ''}</a>`
}).join('')}
</div>
</div>
`34 35 36 37 38
</div>
</div>
</div>
</body>
</html>
`34 35 36 37 38 39 40
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="${data.reposPath}/frontend/main-frontend.bundle.js"></script>
</body>
</html>
`56 57 58 59 60 61
<div class="row align-items-center">
<div class="col-12">
<div class="header-button-container">
<button class="btn btn-info btn-lg dropdown-toggle" id="clone-popover-btn">Clone</button>
${nav.homepageButtons.map((buttonConfig) => {
return `<a class="btn btn-outline-info btn-lg shadow-none" href="${buttonConfig.url}" ${buttonConfig.newTab ? 'target="_blank"' : ''}>${buttonConfig.text}${buttonConfig.newTab ? ' <span>⧉</span>' : ''}</a>`
}).join('')}56 57 58 59 60 61
<div class="row align-items-center">
<div class="col-12">
<div class="header-button-container">
<button class="btn btn-info btn-lg dropdown-toggle clone-popover-btn">Clone</button>
${nav.homepageButtons.map((buttonConfig) => {
return `<a class="btn btn-outline-info btn-lg shadow-none" href="${buttonConfig.url}" ${buttonConfig.newTab ? 'target="_blank"' : ''}>${buttonConfig.text}${buttonConfig.newTab ? ' <span>⧉</span>' : ''}</a>`
}).join('')}141 142 143 144 145
"description": "The root URL where this website will be. E.g.: https://blog.example.com/repos. This URL will be used when a clone or pull command is being shown on your site.",
"type": "string"
},
"path": {
"description": "The path to put the generated site in. All generated files will be put in this directory, repos for cloning will be put in this directory, and it will be added to the end of all URLs used by the default virtual template.",
"type": "string"141 142 143 144 145 146 147 148 149 150 151 152 153 154
"description": "The root URL where this website will be. E.g.: https://blog.example.com/repos. This URL will be used when a clone or pull command is being shown on your site.",
"type": "string"
},
"defaultTemplateConfiguration": {
"additionalProperties": false,
"properties": {
"allRepositoriesPageTitle": {
"type": "string"
}
},
"type": "object"
},
"path": {
"description": "The path to put the generated site in. All generated files will be put in this directory, repos for cloning will be put in this directory, and it will be added to the end of all URLs used by the default virtual template.",
"type": "string"233 234 235 236 237 238 239 240 241 242 243 244 245 246
display: inline-block;
}
.badge.bg-primary {
background: linear-gradient(color.adjust($primary, $lightness: 10%), $primary);
box-shadow: -1px -3px 4px inset color.adjust($primary, $lightness: -30%);
}
.badge.bg-info {
background: linear-gradient(color.adjust($info, $lightness: 10%), $info);
box-shadow: -1px -3px 4px inset color.adjust($info, $lightness: -30%);
}
.header-button-container {
display: flex;233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
display: inline-block;
}
@each $color, $value in $bezel-colors {
.badge.bg-#{$color} {
background: linear-gradient(color.adjust($value, $lightness: 10%), $value);
box-shadow: -1px -3px 4px inset color.adjust($value, $lightness: -30%);
}
}
// .badge.bg-primary {
// background: linear-gradient(color.adjust($primary, $lightness: 10%), $primary);
// box-shadow: -1px -3px 4px inset color.adjust($primary, $lightness: -30%);
// }
//
// .badge.bg-info {
// background: linear-gradient(color.adjust($info, $lightness: 10%), $info);
// box-shadow: -1px -3px 4px inset color.adjust($info, $lightness: -30%);
// }
.header-button-container {
display: flex;40 41 42 43 44
*/
path?: string,
useDefaultTemplate?: boolean,
}
export type GitConfig = {40 41 42 43 44 45 46 47
*/
path?: string,
useDefaultTemplate?: boolean,
defaultTemplateConfiguration?: {
allRepositoriesPageTitle?: string,
},
}
export type GitConfig = {