make clone work, add static JS files for frontend

79d4a6719b7cafb44aea60b1544f0de50c471a24

Tucker McKnight | Sun Aug 24 2025

make clone work, add static JS files for frontend
frontend/main.ts:0
Before
0
After
0
(function() {
  const setCheckbox = (<any>window).setCheckbox
  const currentTheme = (<any>window).currentTheme

  setCheckbox(currentTheme, document.getElementById('dark-mode-switch'))

  const copyCommand = (event) => {
    const elem = event.target
    const cloneText = elem.dataset.cloneUrl
    const originalInnerText = elem.innerText
    navigator.clipboard.writeText(cloneText).then(() => {
      elem.innerText = "Copied"
    })

    window.setTimeout(() => {
      elem.innerText = originalInnerText
    }, 5000)
  }

  const createClonePopover = (currentRepo: string) => {
    const div = document.createElement('div')
    div.id = "clone-popover"
    div.innerHTML = jsVars.cloneDiv
    const popoverBtn = document.getElementById("clone-popover-btn")
    const bsPopover = new bootstrap.Popover(popoverBtn, {
      sanitize: false,
      html: true,
      content: div,
      title: 'Clone',
    })

    div.querySelector("#clone-button").addEventListener('click', copyCommand)

    document.body.addEventListener('click', (event) => {
      const target = event.target as HTMLElement
      // 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) => {
    const isOn = event.target.checked
    const annotations = document.getElementById("annotations")
    if (isOn) {
      annotations.classList.remove("d-none")
    } else {
      annotations.classList.add("d-none")
    }
  }

  document.getElementById("showLastTouch")?.addEventListener('click', toggleLastTouch)

  const copyPull = (event) => {
    const hash = event.target.dataset.hash
    const isDarcs = event.target.dataset.vcs === "darcs"
    const copiedPrefix = isDarcs ? `darcs pull ${jsVars.baseUrl} -h ` : ""
    const originalInnerHtml = event.target.innerHTML
    const copiedAlert = document.createElement('span')
    copiedAlert.innerText = "Copied"

    navigator.clipboard.writeText(`${copiedPrefix}${hash}`).then(() => {
      event.target.parentElement.appendChild(copiedAlert)
    })

    window.setTimeout(() => {
      copiedAlert.remove()
    }, 5000)
  }

  document.querySelectorAll(".copy-btn").forEach((element) => {
    element.addEventListener("click", copyPull)
  })

  const bootstrap = (<any>window).bootstrap
  const jsVars = (<any>window).jsVars

  if (jsVars.nav.repoName) {
    createClonePopover(jsVars.nav.repoName)
  }
}())
frontend/top.ts:0
Before
0
After
0
(function() {
  const htmlElem = document.querySelector('html')
  window['setMode'] = (mode: string) => {
    localStorage.setItem('theme', mode)
    if (mode === 'dark') {
      htmlElem.setAttribute('data-bs-theme', mode)
    }
    else if (mode === 'light') {
      htmlElem.setAttribute('data-bs-theme', mode)
    }
    else if (mode === 'auto') {

      const preferred = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
      htmlElem.setAttribute('data-bs-theme', preferred)
    }
    window['currentTheme'] = mode
  };

  window['currentTheme'] = localStorage.getItem("theme") || "auto"

  window['setMode'](window['currentTheme'])

  window['setCheckbox'] = (mode, element) => {
    if (mode === 'light') {
      element.innerHTML = `<i class="bi bi-brightness-high"></i>`
    }
    if (mode === 'dark') {
      element.innerHTML = `<i class="bi bi-moon"></i>`
    }
    if (mode === 'auto') {
      element.innerHTML = `<i class="bi bi-yin-yang"></i>`
    }
    const link: any = document.getElementById("prism-theme")
    const preferred = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
    const stylesheet = window['currentTheme'] === 'dark' || (window['currentTheme'] === 'auto' && preferred === 'dark') ? "/vendor/prism_dark.css" : "/vendor/prism.css"
    link.href = stylesheet
  }
  window['toggleDarkMode'] = (button) => {
    const clickedOption = button.dataset.themePref
    window['setMode'](clickedOption)
    window['setCheckbox'](clickedOption, document.getElementById('dark-mode-switch'))
  }
}())
frontend/tsconfig.json:0
Before
0
After
0
{
  "compilerOptions": {
    "outDir": "../dist/frontend",
  }
}
main.ts:21
Before
21
After
21
  const vendor = `${__dirname}/vendor`
  const frontend = `${__dirname}/frontend`

  eleventyConfig.addPassthroughCopy({
    [vendor]: "vendor",
    [frontend]: "frontend"
  })

  eleventyConfig.on(
    "eleventy.after",
    async ({ directories, results, runMode, outputMode }) => {
      // Check to see if there is already a repo in all of the locations
      // that should have one.
      for (let repoName in reposConfiguration.repos) {
        const repoConfig = reposConfiguration.repos[repoName]
        if (repoConfig._type === "darcs") {
          for (let branch in repoConfig.branches) {
            const repoPath = eleventyConfig.dir.output + "/repos/" + eleventyConfig.getFilter("slugify")(repoName) + "/branches/" + branch
            const originalLocation = repoConfig.branches[branch].location
            // If it is there, do darcs pull
            if (fsImport.existsSync(repoPath + "/_darcs")) {
              await exec(`(cd ${repoPath} && darcs pull --no-interactive)`)
            } else {
              // If it is not there, do darcs clone
              await exec(`(cd ${repoPath} && mkdir -p temp; darcs clone ${originalLocation} temp/${branch} --no-working-dir; mv temp/${branch}/_darcs .; rm -R temp)`)
            }
          }
        } else if (repoConfig._type === "git") {
          const repoPath = eleventyConfig.dir.output + "/repos/" + eleventyConfig.getFilter("slugify")(repoName)
          const gitRepoName = eleventyConfig.getFilter("slugify")(repoName) + ".git"
          // If it is there, do git pull
          if (fsImport.existsSync(repoPath + ".git")) {
            // git repos are just in the repos folder, not in their subdir
            // create string of commands saying 'git fetch origin branch:branch' for each branch
            const fetchCommands = repoConfig.branchesToPull.map(branch => `git fetch origin ${branch}:${branch}`).join('; ')
            await exec(`(cd ${eleventyConfig.dir.output + "/repos/" + gitRepoName} && ${fetchCommands}; git update-server-info)`)
          } else {
            // If it is not there, do git clone
            const originalLocation = repoConfig.location
            await exec(`(cd ${eleventyConfig.dir.output + "/repos/"} && git clone ${originalLocation} ${gitRepoName} --bare)`)
            await exec(`(cd ${eleventyConfig.dir.output + "/repos/" + gitRepoName} && git update-server-info)`)
          }
        }
      }
    }
  );
main.ts:166
Before
166
      branches: branchesData,
After
166
main.ts:221
Before
221
      branches: branchesData,
After
221
main.ts:248
Before
248
      branches: branchesData,
After
248
main.ts:265
Before
265
After
265
  eleventyConfig.addGlobalData("branches", branchesData)
make.sh:5
Before
5
After
5
cp -r vendor dist/
package.json:3
Before
3
    "build": "./make.sh && npx tsc"
After
3
    "build": "./make.sh && npx tsc &amp;& npx tsc --project frontend&quot;
partial_templates/main_bottom.njk:24
Before
24
    <script src="/static/main.js"></script>
After
24
    <script src="/frontend/main.js"></script>
partial_templates/main_top.njk:29
Before
29
30
    <script src="/static/top.js"></script>
    <link rel="stylesheet" id="prism-theme" type="text/css" href="/prism.css" />
After
29
30
    <script src="/frontend/top.js"></script>
    <link rel="stylesheet" id="prism-theme" type="text/css" href="/vendor/prism.css" />
vendor/prism.css:0
Before
0
After
0
/* PrismJS 1.30.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers+keep-markup */
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:none}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
vendor/prism_dark.css:0
Before
0
After
0
/* PrismJS 1.30.0
https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript&plugins=line-numbers+keep-markup */
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:none}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}