A modern frontend for Github repositories (ironically hosted on Tangled)
Readme.vue
<script setup lang="ts">
defineProps<{
  html: string;
}>();

const router = useRouter();
const { copy } = useClipboard();

// Combined click handler for:
// 1. Intercepting GitHub links to route internally (if needed)
// 2. Copy button functionality for code blocks
function handleClick(event: MouseEvent) {
  const target = event.target as HTMLElement | undefined;
  if (!target) return;

  // Handle copy button clicks
  const copyTarget = target.closest("[data-copy]");
  if (copyTarget) {
    const wrapper = copyTarget.closest(".readme-code-block");
    if (!wrapper) return;

    const pre = wrapper.querySelector("pre");
    if (!pre?.textContent) return;

    copy(pre.textContent);

    const icon = copyTarget.querySelector("span");
    if (!icon) return;

    const originalIcon = "i-heroicons-document-duplicate";
    const successIcon = "i-heroicons-check";

    icon.classList.remove(originalIcon);
    icon.classList.add(successIcon);

    setTimeout(() => {
      icon.classList.remove(successIcon);
      icon.classList.add(originalIcon);
    }, 2000);
  }
}
</script>

<template>
  <article
    class="readme prose prose-gray dark:prose-invert max-w-none"
    v-html="html"
    @click="handleClick"
  />
</template>

<style scoped>
/* README prose styling */
.readme {
  line-height: 1.75;
  /* Prevent horizontal overflow on mobile */
  overflow-wrap: break-word;
  word-wrap: break-word;
  word-break: break-word;
  /* Contain all children */
  overflow: hidden;
  min-width: 0;
}

@media screen and (prefers-color-scheme: light) {
  .readme {
    color: #000;
  }

  .readme svg {
    filter: invert(1);
  }
}

@media screen and (prefers-color-scheme: dark) {
  .readme {
    color: #fff;
  }

  .readme svg {
    filter: invert(1);
  }
}

/* README headings */
.readme :deep(h1),
.readme :deep(h2),
.readme :deep(h3),
.readme :deep(h4),
.readme :deep(h5),
.readme :deep(h6) {
  font-family: "Monaspace Krypton", ui-monospace, SFMono-Regular, monospace;
  font-weight: 500;
  margin-top: 2rem;
  margin-bottom: 1rem;
  line-height: 1.3;
}

.readme :deep(h1) {
  font-size: 1.875rem;
  border-bottom: 1px solid rgb(229 231 235);
  padding-bottom: 0.5rem;
}

.readme :deep(h2) {
  font-size: 1.5rem;
  border-bottom: 1px solid rgb(229 231 235);
  padding-bottom: 0.5rem;
}

.readme :deep(h3) {
  font-size: 1.25rem;
}

.readme :deep(h4) {
  font-size: 1.125rem;
}

.readme :deep(h5) {
  font-size: 1rem;
}

.readme :deep(h6) {
  font-size: 0.875rem;
}

.readme :deep(p) {
  margin-bottom: 1rem;
  color: rgb(55 65 81);
}

.readme :deep(a) {
  color: rgb(37 99 235);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.readme :deep(a:hover) {
  color: rgb(30 64 175);
}

.readme :deep(code) {
  font-family: "Monaspace Krypton", ui-monospace, SFMono-Regular, monospace;
  background: rgb(243 244 246);
  padding: 0.375rem 0.625rem;
  border-radius: 0.25rem;
  font-size: 0.875rem;
  color: rgb(31 41 55);
}

/* Code blocks */
.readme :deep(pre) {
  background: rgb(249 250 251);
  border: 1px solid rgb(229 231 235);
  border-radius: 0.5rem;
  padding: 1rem;
  overflow-x: auto;
  margin: 1.5rem 0;
  position: relative;
}

.readme :deep(pre code) {
  background: transparent;
  border: none;
  padding: 0;
  font-family: "Monaspace Krypton", ui-monospace, SFMono-Regular, monospace;
  font-size: 0.875rem;
  color: rgb(31 41 55);
}

.readme :deep(ul),
.readme :deep(ol) {
  margin: 1rem 0;
  padding-left: 1.5rem;
}

.readme :deep(li) {
  margin-bottom: 0.5rem;
}

.readme :deep(blockquote) {
  border-left: 4px solid rgb(209 213 219);
  padding-left: 1rem;
  margin: 1.5rem 0;
  color: rgb(75 85 99);
  font-style: italic;
}

.readme :deep(table) {
  width: 100%;
  border-collapse: collapse;
  margin: 1.5rem 0;
  overflow-x: auto;
  display: block;
  white-space: nowrap;
}

.readme :deep(th),
.readme :deep(td) {
  border: 1px solid rgb(209 213 219);
  padding: 0.75rem;
  text-align: left;
}

.readme :deep(th) {
  background: rgb(249 250 251);
  font-weight: 600;
}

.readme :deep(img) {
  max-width: 100%;
  height: auto;
  border-radius: 8px;
  margin: 1rem 0;
}

.readme :deep(hr) {
  border: none;
  border-top: 1px solid rgb(209 213 219);
  margin: 2rem 0;
}

/* Badge images inline */
.readme :deep(p > a > img),
.readme :deep(p > img) {
  display: inline-block;
  margin: 0 0.25rem 0.25rem 0;
  border-radius: 4px;
}

/* Dark mode adjustments */
@media (prefers-color-scheme: dark) {
  .readme :deep(h1),
  .readme :deep(h2) {
    border-bottom-color: rgb(55 65 81);
  }

  .readme :deep(p) {
    color: rgb(209 213 219);
  }

  .readme :deep(a) {
    color: rgb(96 165 250);
  }

  .readme :deep(a:hover) {
    color: rgb(147 197 253);
  }

  .readme :deep(code) {
    background: rgb(31 41 55);
    color: rgb(229 231 235);
  }

  .readme :deep(pre) {
    background: rgb(17 24 39);
    border-color: rgb(55 65 81);
  }

  .readme :deep(pre code) {
    color: rgb(229 231 235);
  }

  .readme :deep(blockquote) {
    border-left-color: rgb(75 85 99);
    color: rgb(156 163 175);
  }

  .readme :deep(th),
  .readme :deep(td) {
    border-color: rgb(75 85 99);
  }

  .readme :deep(th) {
    background: rgb(31 41 55);
  }

  .readme :deep(hr) {
    border-top-color: rgb(75 85 99);
  }
}
</style>