[READ-ONLY] a fast, modern browser for the npm registry forked from npmx.dev/npmx.dev
npmx.dev app components
16 folders · 28 files

Readme.vue
<script setup lang="ts">
defineProps<{
  html: string
}>()

const { copy } = useClipboard()

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

  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button) {
    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-lucide:copy'
    const successIcon = 'i-lucide:check'

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

    setTimeout(() => {
      icon.classList.remove(successIcon)
      icon.classList.add(originalIcon)
    }, 2000)
    return
  }

  // Handle npmjs.com link clicks - route internally
  const anchor = target.closest('a')
  if (!anchor) return

  const href = anchor.getAttribute('href')
  if (!href) return

  // Handle relative anchor links
  if (href.startsWith('#') || href.startsWith('/')) {
    event.preventDefault()
    navigateTo(href)
    return
  }
}
</script>

<template>
  <article
    class="readme prose prose-invert max-w-[70ch] lg:max-w-none px-1"
    dir="auto"
    v-html="html"
    :style="{
      '--i18n-note': '\'' + $t('package.readme.callout.note') + '\'',
      '--i18n-tip': '\'' + $t('package.readme.callout.tip') + '\'',
      '--i18n-important': '\'' + $t('package.readme.callout.important') + '\'',
      '--i18n-warning': '\'' + $t('package.readme.callout.warning') + '\'',
      '--i18n-caution': '\'' + $t('package.readme.callout.caution') + '\'',
    }"
    @click="handleClick"
  />
</template>

<style scoped>
/* README prose styling */
.readme {
  color: var(--fg-muted);
  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;
  /* Contain all children z-index values inside this container */
  isolation: isolate;
}

/* README headings - styled by visual level (data-level), not semantic level */
.readme :deep(h3),
.readme :deep(h4),
.readme :deep(h5),
.readme :deep(h6) {
  @apply font-mono scroll-mt-20;
  color: var(--fg);
  font-weight: 500;
  margin-top: 1rem;
  margin-bottom: 1rem;
  line-height: 1.3;

  a {
    text-decoration: none;
  }
}

/* Visual styling based on original README heading level */
.readme :deep([data-level='1']) {
  font-size: 1.5rem;
}
.readme :deep([data-level='2']) {
  font-size: 1.25rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}
.readme :deep([data-level='3']) {
  font-size: 1.125rem;
}
.readme :deep([data-level='4']) {
  font-size: 1rem;
}
.readme :deep([data-level='5']) {
  font-size: 0.925rem;
}
.readme :deep([data-level='6']) {
  font-size: 0.875rem;
}

.readme :deep(p) {
  margin-bottom: 1rem;
}

.readme :deep(a) {
  @apply underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg transition-colors duration-200;
}
.readme :deep(a:hover) {
  @apply decoration-accent text-accent;
}
.readme :deep(a:focus-visible) {
  @apply decoration-accent text-accent;
}

.readme :deep(a[target='_blank']:not(:has(img))::after) {
  /* I don't know what kind of sorcery this is, but it ensures this icon can't wrap to a new line on its own. */
  content: '__';
  @apply inline i-lucide:external-link rtl-flip ms-1 opacity-50;
}

.readme :deep(a[href^='#']::after) {
  /* I don't know what kind of sorcery this is, but it ensures this icon can't wrap to a new line on its own. */
  content: '__';
  @apply inline i-lucide:link rtl-flip ms-1 opacity-0;
}

.readme :deep(a[href^='#']:hover::after) {
  @apply opacity-100;
}

.readme :deep(code) {
  @apply font-mono;
  font-size: 0.875em;
  background: var(--bg-muted);
  padding: 0.2em 0.4em;
  border-radius: 4px;
  border: 1px solid var(--border);
}

/* Code blocks - including Shiki output */
.readme :deep(pre),
.readme :deep(.shiki) {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem;
  overflow-x: auto;
  margin: 1.5rem 0;
  /* Fix horizontal overflow */
  max-width: 100%;
  box-sizing: border-box;
}

.readme :deep(.readme-code-block) {
  @apply bg-bg-subtle;
  display: block;
  width: 100%;
  position: relative;
}

.readme :deep(.readme-copy-button) {
  position: absolute;
  top: 0.4rem;
  inset-inline-end: 0.4rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.25rem;
  border-radius: 6px;
  background: color-mix(in srgb, var(--bg-subtle) 80%, transparent);
  border: 1px solid var(--border);
  color: var(--fg-subtle);
  opacity: 0;
  transition:
    opacity 0.2s ease,
    color 0.2s ease,
    border-color 0.2s ease;
}

.readme :deep(.readme-code-block:hover .readme-copy-button),
.readme :deep(.readme-copy-button:focus-visible) {
  opacity: 1;
}

.readme :deep(.readme-copy-button:hover) {
  color: var(--fg);
  border-color: var(--border-hover);
}

.readme :deep(.readme-copy-button > span) {
  width: 1.05rem;
  height: 1.05rem;
  display: inline-block;
  pointer-events: none;
}

.readme :deep(pre code),
.readme :deep(.shiki code) {
  background: transparent !important;
  border: none;
  padding: 0;
  @apply font-mono;
  font-size: 0.875rem;
  color: var(--fg);
  /* Prevent code from forcing width */
  white-space: pre;
  word-break: normal;
  overflow-wrap: normal;
  /* Makes unicode and ascii art work properly */
  line-height: 1.25;
  display: inline-block;
}

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

.readme :deep(ul) {
  list-style-type: disc;
}

.readme :deep(ol) {
  list-style-type: decimal;
}

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

.readme :deep(li::marker) {
  color: var(--border-hover);
}

.readme :deep(blockquote) {
  border-inline-start: 2px solid var(--border);
  padding-inline-start: 1rem;
  margin: 1.5rem 0;
  color: var(--fg-subtle);
  font-style: italic;
}

/* GitHub-style callouts/alerts */
.readme :deep(blockquote[data-callout]) {
  border-inline-start-width: 3px;
  padding: 1rem;
  padding-inline-start: 1.25rem;
  background: var(--bg-subtle);
  font-style: normal;
  color: var(--fg-subtle);
  position: relative;
}

.readme :deep(blockquote[data-callout]::before) {
  display: block;
  @apply font-mono;
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.5rem;
  padding-inline-start: 1.5rem;
}

.readme :deep(blockquote[data-callout]::after) {
  content: '';
  width: 1.25rem;
  height: 1.25rem;
  position: absolute;
  top: 1rem;
  left: 1rem;
}

.readme :deep(blockquote[data-callout] > p:first-child) {
  margin-top: 0;
}

.readme :deep(blockquote[data-callout] > p:last-child) {
  margin-bottom: 0;
}

/* Note - blue */
.readme :deep(blockquote[data-callout='note']) {
  border-inline-start-color: var(--syntax-str);
  background: rgba(59, 130, 246, 0.05);
}
.readme :deep(blockquote[data-callout='note']::before) {
  content: var(--i18n-note, 'Note');
  color: #3b82f6;
}
.readme :deep(blockquote[data-callout='note']::after) {
  background-color: #3b82f6;
  -webkit-mask: icon('i-lucide:info') no-repeat;
  mask: icon('i-lucide:info') no-repeat;
}

/* Tip - green */
.readme :deep(blockquote[data-callout='tip']) {
  border-inline-start-color: #22c55e;
  background: rgba(34, 197, 94, 0.05);
}
.readme :deep(blockquote[data-callout='tip']::before) {
  content: var(--i18n-tip, 'Tip');
  color: #22c55e;
}
.readme :deep(blockquote[data-callout='tip']::after) {
  background-color: #22c55e;
  -webkit-mask: icon('i-lucide:lightbulb') no-repeat;
  mask: icon('i-lucide:lightbulb') no-repeat;
}

/* Important - purple */
.readme :deep(blockquote[data-callout='important']) {
  border-inline-start-color: var(--syntax-fn);
  background: rgba(168, 85, 247, 0.05);
}
.readme :deep(blockquote[data-callout='important']::before) {
  content: var(--i18n-important, 'Important');
  color: var(--syntax-fn);
}
.readme :deep(blockquote[data-callout='important']::after) {
  background-color: var(--syntax-fn);
  -webkit-mask: icon('i-lucide:pin') no-repeat;
  mask: icon('i-lucide:pin') no-repeat;
}

/* Warning - yellow/orange */
.readme :deep(blockquote[data-callout='warning']) {
  border-inline-start-color: #eab308;
  background: rgba(234, 179, 8, 0.05);
}
.readme :deep(blockquote[data-callout='warning']::before) {
  content: var(--i18n-warning, 'Warning');
  color: #eab308;
}
.readme :deep(blockquote[data-callout='warning']::after) {
  background-color: #eab308;
  -webkit-mask: icon('i-lucide:triangle-alert') no-repeat;
  mask: icon('i-lucide:triangle-alert') no-repeat;
}

/* Caution - red */
.readme :deep(blockquote[data-callout='caution']) {
  border-inline-start-color: #ef4444;
  background: rgba(239, 68, 68, 0.05);
}
.readme :deep(blockquote[data-callout='caution']::before) {
  content: var(--i18n-caution, 'Caution');
  color: #ef4444;
}
.readme :deep(blockquote[data-callout='caution']::after) {
  background-color: #ef4444;
  -webkit-mask: icon('i-lucide:circle-alert') no-repeat;
  mask: icon('i-lucide:circle-alert') no-repeat;
}

/* Table wrapper for horizontal scroll on mobile */
.readme :deep(table) {
  display: block;
  width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  margin: 1.5rem 0;
  font-size: 0.875rem;
  word-break: keep-all;
}

.readme :deep(th),
.readme :deep(td) {
  border: 1px solid var(--border);
  padding: 0.75rem 1rem;
  text-align: start;
}

.readme :deep(th) {
  background: var(--bg-subtle);
  color: var(--fg);
  font-weight: 500;
}

.readme :deep(tr:hover) {
  background: var(--bg-subtle);
}

.readme :deep(img) {
  max-width: 100%;
  height: revert-layer;
  display: revert-layer;
  border-radius: 8px;
  margin: 1rem 0;
  position: relative;
  z-index: 1;
}

.readme :deep(video) {
  height: revert-layer;
  display: revert-layer;
}

.readme :deep(hr) {
  border: none;
  border-top: 1px solid var(--border);
  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;
}

/* Screen reader only text */
.readme :deep(.sr-only) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
</style>