Add clone buttons and homepage buttons to index

6122634d2ba8175cf3dfc6ff1bd73a2c6f1e420e

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.
frontend/main.js:3
Before
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.target
After
2
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.target
frontend/main.js:18
Before
17
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,
After
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,
frontend/main.js:40
Before
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) => {
After
39
40
41










42


43
44

    container: 'body',
  })

⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
  window.popovers.push(bsPopover)
⁣
⁣
}

⁣
⁣
const toggleLastTouch = (event) => {
frontend/main.js:99
Before
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)
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
After
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())
  }
})
js_templates/commit.ts:13
Before
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>
After
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>
js_templates/commit.ts:40
Before
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>
After
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>
js_templates/common/branchesListItems.ts:1
Before
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>`
After
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>`
js_templates/common/htmlPage.ts:112
Before
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>
After
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>
js_templates/file.ts:14
Before
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">&#x1F3E0; ./</a></span>${
            data.fileInfo.file.split('/').map((dir, index, arr) => {
              if (index === arr.length - 1) {
                return `<span class="px-2">${dir}</span>`
After
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>`
js_templates/file.ts:71
Before
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>
After
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>
js_templates/files.ts:11
Before
10
11
12
13
14
15
    <div class="row my-3">
      <div class="col">
        <h3>
          <span>&#x1F3E0; ./</span>
        </h3>
      </div>
    </div>
After
10
11
12
13
14
15
    <div class="row my-3">
      <div class="col">
        <h3>
          <span>./</span>
        </h3>
      </div>
    </div>
js_templates/index.ts:15
Before
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>
                  `
After
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>&#x29C9;</span>' : ''}</a>`
                        }).join('')}
                      </div>
                    </div>
                  `
js_templates/index.ts:35
Before
34
35
36


37
38
            </div>
          </div>
        </div>
⁣
⁣
      </body>
    </html>
  `
After
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>
  `
js_templates/repo.ts:57
Before
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>&#x29C9;</span>' : ''}</a>`
                    }).join('')}
After
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>&#x29C9;</span>' : ''}</a>`
                    }).join('')}
schemas/ReposConfiguration.json:142
Before
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"
After
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"
scss/design-board.scss:234
Before
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;
After
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;
src/configTypes.ts:41
Before
40
41
42


43
44
  */
  path?: string,
  useDefaultTemplate?: boolean,
⁣
⁣
}

⁣
export type GitConfig = {
After
40
41
42
43
44
45
46
47
  */
  path?: string,
  useDefaultTemplate?: boolean,
  defaultTemplateConfiguration?: {
    allRepositoriesPageTitle?: string,
  },
}

export type GitConfig = {