Make language percent breakdown work; add optional config for extra buttons

073785ab9fa931c3d89e9631c36b2ff05e1b14ee

Tucker McKnight <tucker@pangolin.lan> | Wed Dec 24 2025

Make language percent breakdown work; add optional config for extra buttons
frontend/top.js:23
Before
22
23
24
25
26
27
  window['setCheckbox'] = (mode, element) => {
    if (element === null) { return }
    if (mode === 'light') {
      element.innerHTML = `<span>&#x1F4A1;</span>`
    }
    if (mode === 'dark') {
      element.innerHTML = `<span>&#x1F319;</span>`
After
22
23
24
25
26
27
  window['setCheckbox'] = (mode, element) => {
    if (element === null) { return }
    if (mode === 'light') {
      element.innerHTML = `<span>&#x1F31E;</span>`
    }
    if (mode === 'dark') {
      element.innerHTML = `<span>&#x1F319;</span>`
js_templates/common/htmlPage.ts:96
Before
95
96
97
98
99
100
101
102
103
104
              </div>
              <div class="col-auto d-flex align-items-center">
                <div class="dropdown">
                  <button class="dropdown-toggle btn shadow-none" id="dark-mode-switch" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                    <span>&#xFE0F;</span>
                  </button>
                  <ul class="dropdown-menu">
                    <li><button class="btn shadow-none" data-theme-pref="light" onclick="toggleDarkMode(this)"><span class="me-1">&#x1F4A1;</span>Light</button></li>
                    <li><button class="btn shadow-none" data-theme-pref="dark" onclick="toggleDarkMode(this)"><span class="me-1">&#x1F319;</span>Dark</button></li>
                    <li><button class="btn shadow-none" data-theme-pref="auto" onclick="toggleDarkMode(this)"><span class="me-1">&#x1F5A5;&#xFE0F;</span>Match OS</button></li>
                  </ul>
After
95
96
97
98
99
100
101
102
103
104
              </div>
              <div class="col-auto d-flex align-items-center">
                <div class="dropdown">
                  <button class="dropdown-toggle btn btn-bg" id="dark-mode-switch" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                    <span>&#xFE0F;</span>
                  </button>
                  <ul class="dropdown-menu">
                    <li><button class="btn shadow-none" data-theme-pref="light" onclick="toggleDarkMode(this)"><span class="me-1">&#x1F31E;</span>Light</button></li>
                    <li><button class="btn shadow-none" data-theme-pref="dark" onclick="toggleDarkMode(this)"><span class="me-1">&#x1F319;</span>Dark</button></li>
                    <li><button class="btn shadow-none" data-theme-pref="auto" onclick="toggleDarkMode(this)"><span class="me-1">&#x1F5A5;&#xFE0F;</span>Match OS</button></li>
                  </ul>
js_templates/helpers/nav.ts:36
Before
35
36
37

38
39
    repoBranchCommitsBase: () => repoBranchCommitsBase,
    repoCurrentBranchCommits: () => `${repoBranchCommitsBase}page1`,
    repoCurrentBranchBranches: () => `${currentBranchPath}/branches`,
⁣
  }
}
After
35
36
37
38
39
40
    repoBranchCommitsBase: () => repoBranchCommitsBase,
    repoCurrentBranchCommits: () => `${repoBranchCommitsBase}page1`,
    repoCurrentBranchBranches: () => `${currentBranchPath}/branches`,
    homepageButtons: reposConfig.repos[repoName].defaultTemplateConfiguration?.homepageButtons
  }
}
js_templates/repo.ts:7
Before
6
7
8
9




























10
  const renderContentIfAvailable = eleventyConfig.getFilter("renderContentIfAvailable")
  const getReadMe = eleventyConfig.getFilter("getReadMe")
  const latestCommit = repo.commits.get(branch.head)

⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
  return `
    <div class="row">
After
6
7
8
9
10
11
12
13
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
  const renderContentIfAvailable = eleventyConfig.getFilter("renderContentIfAvailable")
  const getReadMe = eleventyConfig.getFilter("getReadMe")
  const latestCommit = repo.commits.get(branch.head)
  const latestCommitMessage = latestCommit.message.length > 72
    ? latestCommit.message.split('\n')[0].substr(0, 72) + '...'
    : latestCommit.message

  const languageCounts = branch.fileList.reduce((counts, currentFile) => {
    const fileParts = currentFile.split(".")
    const fileExtension = fileParts[fileParts.length - 1]

    counts.set(fileExtension, (counts.get(fileExtension) + 1) || 1)
    return counts
  }, new Map<string, number>())

  // todo: this is probably broken for repos that use fewer than 6 languages
  let languagePercentages: Array<[string, number]> = []
  const total = branch.fileList.length

  for (const entry of languageCounts) {
    languagePercentages.push([entry[0], entry[1] / total])
  }
  languagePercentages.sort((a, b) => {
    return b[1] - a[1]
  })
  const topLanguagePercentages = languagePercentages.slice(0, 5)
  const otherLanguagePercent = languagePercentages.slice(6, languagePercentages.length - 6).reduce((sum, current) => {
    return sum + current[1]
  }, 0)

  const largestPercent = Math.max(...topLanguagePercentages.map(tuple => tuple[1]), otherLanguagePercent)

  return `
    <div class="row">
js_templates/repo.ts:17
Before
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

43
44
45
46
47


48
49
              <h1 class="display-2 text-white"><em>${repo.name}</em></h1>
              ${repo.description ? '<p class="text-white fs-5">' + repo.description + '</p>' : ''}
            </div>
            <div class="col-12 col-xl-6 d-flex flex-column">
              <div class="row py-3 flex-grow-1 align-items-center">
                <div class="col-12">
                  <div class="row">
                    <div class="col-12 language-percentages">
                      <span style="width: 25%;" class="language-name text-light font-monospace">HTML</span>
                      <span style="width: 15%;" class="language-name text-light font-monospace">CSS</span>
                      <span style="width: 10%;" class="language-name text-light font-monospace">Rust</span>
                      <span style="width: 33%;" class="language-name text-light font-monospace">Typescript</span>
                      <span style="width: 17%;" class="language-name text-light font-monospace">Bash</span>
                    </div>
                  </div>
                  <div class="row">
                    <div class="col-12 language-percentages">
                      <div style="background: #E273FA; width: 25%;" class="language-percent"></div>
                      <div style="background: #852CDF; width: 15%;" class="language-percent"></div>
                      <div style="background: #E273FA; width: 10%;" class="language-percent"></div>
                      <div style="background: #852CDF; width: 33%;" class="language-percent"></div>
                      <div style="background: #E273FA; width: 17%;" class="language-percent"></div>
                    </div>
                  </div>
                </div>
              </div>
              <div class="row flex-grow-1 align-items-center">
⁣
                <div class="col-md col-fluid py-3">
                  <button class="w-100 btn btn-info btn-lg dropdown-toggle" id="clone-popover-btn">Clone</button>
                </div>
                <div class="col-md col-fluid py-3">
                  <button class="w-100 btn btn-outline-info shadow-none btn-lg">Setup Instructions</button>
⁣
⁣
                </div>
              </div>
            </div>
After
16
17
18
19
20
21

22






23







24
25
26
27
28
29
30
31

32
33
34
35
36
37
              <h1 class="display-2 text-white"><em>${repo.name}</em></h1>
              ${repo.description ? '<p class="text-white fs-5">' + repo.description + '</p>' : ''}
            </div>
            <div class="col-12 col-xl-6 d-flex flex-column justify-content-around">
              <div class="row flex-grow-1 align-items-stretch language-percent-row">
                <div class="col-12 d-flex align-items-stretch">
⁣
                  <div class="row d-flex flex-grow-1">
⁣
⁣
⁣
⁣
⁣
⁣
                    ${topLanguagePercentages.map((percentTuple) => `<div class='col language-col'><div class="language-name text-light font-monospace">${percentTuple[0]}</div><div class="language-percent" style="flex-grow: ${percentTuple[1] / largestPercent}"></div></div>`).join('')}
⁣
⁣
⁣
⁣
⁣
⁣
⁣
                  <div class='col language-col'><div class="language-name text-light font-monospace">other</div><div class="language-percent" style="flex-grow: ${otherLanguagePercent / largestPercent}"></div></div>
                  </div>
                </div>
              </div>
              <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('')}
                  </div>
                </div>
              </div>
            </div>
js_templates/repo.ts:61
Before
60
61
62
63
64
65
66
          </noscript>
          <div class="row mt-2">
            <div class="col">
              <p class="font-monospace text-white text-truncate">
                Latest commit: ${latestCommit.date.toDateString()} <a class="fw-bold link-info" href="${nav.repoBranchCommitsBase()}${latestCommit.hash}">${latestCommit.hash.substr(0, 6)}</a> ${latestCommit.message.split('\n')[0]}
              </p>
            </div>
          </div>
After
60
61
62
63
64
65
66
          </noscript>
          <div class="row mt-2">
            <div class="col">
              <p class="font-monospace text-white">
                Latest commit: ${latestCommit.date.toDateString()} <a class="fw-bold link-info" href="${nav.repoBranchCommitsBase()}${latestCommit.hash}">${latestCommit.hash.substr(0, 6)}</a> ${latestCommitMessage}
              </p>
            </div>
          </div>
schemas/ReposConfiguration.json:57
Before
56
57
58































59
60
        "defaultBranch": {
          "type": "string"
        },
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
        "description": {
          "type": "string"
        },
After
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
        "defaultBranch": {
          "type": "string"
        },
        "defaultTemplateConfiguration": {
          "additionalProperties": false,
          "properties": {
            "homepageButtons": {
              "items": {
                "additionalProperties": false,
                "properties": {
                  "newTab": {
                    "type": "boolean"
                  },
                  "text": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  }
                },
                "required": [
                  "url",
                  "text"
                ],
                "type": "object"
              },
              "type": "array"
            }
          },
          "required": [
            "homepageButtons"
          ],
          "type": "object"
        },
        "description": {
          "type": "string"
        },
schemas/ReposConfiguration.json:121
Before
120
121
122
123



124
            "description": "An object containing the configuration for your repositories. Each key in this object is a repository name, and the value has several config options for that repository. The required config options describe the path to the repository and which branches should be pulled. See the specific definition of  {@link  GitConfig }  for more details about what goes in these configuration objects."
          },
          "type": "object"
        }
⁣
⁣
⁣
      },
      "required": [
After
120
121
122
123
124
125
126
127
            "description": "An object containing the configuration for your repositories. Each key in this object is a repository name, and the value has several config options for that repository. The required config options describe the path to the repository and which branches should be pulled. See the specific definition of  {@link  GitConfig }  for more details about what goes in these configuration objects."
          },
          "type": "object"
        },
        "useDefaultTemplate": {
          "type": "boolean"
        }
      },
      "required": [
scss/design-board.scss:40
Before
39
40
41
42
43
44
45
46
47
48
49
  background-color: $lightblue;
  border-top: 6px solid color.adjust($lightblue, $lightness: 15%);
  border-left: 6px solid color.adjust($lightblue, $lightness: 13%);
  border-bottom: 6px solid color.adjust($lightblue, $lightness: -10%);
  border-right: 6px solid color.adjust($lightblue, $lightness: -13%);
}

#dark-mode-switch span {
  text-shadow: 0 0 12px $black;
}

@include color-mode(dark) {
After
39
40
41
42
43
44
45



  background-color: $lightblue;
  border-top: 6px solid color.adjust($lightblue, $lightness: 15%);
  border-left: 6px solid color.adjust($lightblue, $lightness: 13%);
  border-bottom: 6px solid color.adjust($lightblue, $lightness: -30%);
  border-right: 6px solid color.adjust($lightblue, $lightness: -40%);
}

⁣
⁣
⁣
⁣
@include color-mode(dark) {
scss/design-board.scss:78
Before
77
78
79
80

81
82
83
84
85
86
87
    border-bottom: 6px solid color.adjust($body-bg-dark, $lightness: -5%);
    border-right: 6px solid color.adjust($body-bg-dark, $lightness: -9%);
  }

⁣
  #dark-mode-switch span {
    text-shadow: 0 0 12px $white;
  }
}

.bezel-purple {
  background-color: $purple;
  border-top: 6px solid color.adjust($purple, $lightness: 15%);
After
77
78
79
80
81
82
83
84
85
86
87
88
    border-bottom: 6px solid color.adjust($body-bg-dark, $lightness: -5%);
    border-right: 6px solid color.adjust($body-bg-dark, $lightness: -9%);
  }
}

.btn-bg, .btn-bg:hover {
  border: 1px solid $body-color;
}


.bezel-purple {
  background-color: $purple;
  border-top: 6px solid color.adjust($purple, $lightness: 15%);
scss/design-board.scss:165
Before
164
165
166



167
168
169
170





171
172
173
174
175

176
177



  width: 1rem;
}

⁣
⁣
⁣
.language-percentages {
  font-size: 0;
}

⁣
⁣
⁣
⁣
⁣
.language-percent {
  display: inline-block;
  margin: 0;
  padding: 0;
  height: 2rem;
⁣
}

⁣
⁣
⁣
⁣
.language-name {
After
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
  width: 1rem;
}

.language-percent-row {
  max-height: 60%;
}
.language-percentages {
  font-size: 0;
}
.language-col {
  display: flex;
  flex-direction: column;
  justify-content: end;
  min-height: 4rem;
}
.language-percent {
  display: inline-block;
  margin: 0;
  padding: 0;
  background-color: $lilac;
  // background-color: #E273FA;
}
.language-col:nth-of-type(2n) .language-percent {
  background-color: $magenta;
  // background-color: #852CDF;
}

.language-name {
scss/design-board.scss:181
Before
180
181
182
183
184
185
186
187
188
  font-size: 1rem;
}

.branches, .branches:hover {
  border: 1px solid $body-color;
}

.dropdown-branches {
  list-style: none;
  padding: 0;
After
180
181
182




183
184
  font-size: 1rem;
}

⁣
⁣
⁣
⁣
.dropdown-branches {
  list-style: none;
  padding: 0;
scss/design-board.scss:210
Before
209
210





















.sortRadioButton {
  display: inline-block;
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
}
After
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
.sortRadioButton {
  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;
  flex-wrap: wrap;
  margin: 10px -10px;

  .btn {
    flex: 1;
    flex-basis: 200px;
    margin: 10px 10px;
  }
}
src/configTypes.ts:40
Before
39
40
41

42
43
  * @example path: "/repos"
  */
  path?: string,
⁣
}

export type GitConfig = {
After
39
40
41
42
43
44
  * @example path: "/repos"
  */
  path?: string,
  useDefaultTemplate?: boolean,
}

export type GitConfig = {
src/configTypes.ts:58
Before
57
58
59
60






      command: string,
      copyFrom: string,
      copyTo: string,
  }[]
⁣
⁣
⁣
⁣
⁣
⁣
⁣
}
After
57
58
59
60
61
62
63
64
65
66
67
      command: string,
      copyFrom: string,
      copyTo: string,
  }[],
  defaultTemplateConfiguration?: {
    homepageButtons: Array<{
      url: string,
      text: string,
      newTab?: boolean,
    }>
  },
}