[READ-ONLY] a fast, modern browser for the npm registry
at main 459 lines 11 kB view raw
1<script setup lang="ts"> 2defineProps<{ 3 html: string 4}>() 5 6const { copy } = useClipboard() 7 8// Combined click handler for: 9// 1. Intercepting npmjs.com links to route internally 10// 2. Copy button functionality for code blocks 11function handleClick(event: MouseEvent) { 12 const target = event.target as HTMLElement | undefined 13 if (!target) return 14 15 if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button) { 16 return 17 } 18 19 // Handle copy button clicks 20 const copyTarget = target.closest('[data-copy]') 21 if (copyTarget) { 22 const wrapper = copyTarget.closest('.readme-code-block') 23 if (!wrapper) return 24 25 const pre = wrapper.querySelector('pre') 26 if (!pre?.textContent) return 27 28 copy(pre.textContent) 29 30 const icon = copyTarget.querySelector('span') 31 if (!icon) return 32 33 const originalIcon = 'i-lucide:copy' 34 const successIcon = 'i-lucide:check' 35 36 icon.classList.remove(originalIcon) 37 icon.classList.add(successIcon) 38 39 setTimeout(() => { 40 icon.classList.remove(successIcon) 41 icon.classList.add(originalIcon) 42 }, 2000) 43 return 44 } 45 46 // Handle npmjs.com link clicks - route internally 47 const anchor = target.closest('a') 48 if (!anchor) return 49 50 const href = anchor.getAttribute('href') 51 if (!href) return 52 53 // Handle relative anchor links 54 if (href.startsWith('#') || href.startsWith('/')) { 55 event.preventDefault() 56 navigateTo(href) 57 return 58 } 59} 60</script> 61 62<template> 63 <article 64 class="readme prose prose-invert max-w-[70ch] lg:max-w-none px-1" 65 dir="auto" 66 v-html="html" 67 :style="{ 68 '--i18n-note': '\'' + $t('package.readme.callout.note') + '\'', 69 '--i18n-tip': '\'' + $t('package.readme.callout.tip') + '\'', 70 '--i18n-important': '\'' + $t('package.readme.callout.important') + '\'', 71 '--i18n-warning': '\'' + $t('package.readme.callout.warning') + '\'', 72 '--i18n-caution': '\'' + $t('package.readme.callout.caution') + '\'', 73 }" 74 @click="handleClick" 75 /> 76</template> 77 78<style scoped> 79/* README prose styling */ 80.readme { 81 color: var(--fg-muted); 82 line-height: 1.75; 83 /* Prevent horizontal overflow on mobile */ 84 overflow-wrap: break-word; 85 word-wrap: break-word; 86 word-break: break-word; 87 /* Contain all children */ 88 overflow: hidden; 89 min-width: 0; 90 /* Contain all children z-index values inside this container */ 91 isolation: isolate; 92} 93 94/* README headings - styled by visual level (data-level), not semantic level */ 95.readme :deep(h3), 96.readme :deep(h4), 97.readme :deep(h5), 98.readme :deep(h6) { 99 @apply font-mono scroll-mt-20; 100 color: var(--fg); 101 font-weight: 500; 102 margin-top: 1rem; 103 margin-bottom: 1rem; 104 line-height: 1.3; 105 106 a { 107 text-decoration: none; 108 } 109} 110 111/* Visual styling based on original README heading level */ 112.readme :deep([data-level='1']) { 113 font-size: 1.5rem; 114} 115.readme :deep([data-level='2']) { 116 font-size: 1.25rem; 117 padding-bottom: 0.5rem; 118 border-bottom: 1px solid var(--border); 119} 120.readme :deep([data-level='3']) { 121 font-size: 1.125rem; 122} 123.readme :deep([data-level='4']) { 124 font-size: 1rem; 125} 126.readme :deep([data-level='5']) { 127 font-size: 0.925rem; 128} 129.readme :deep([data-level='6']) { 130 font-size: 0.875rem; 131} 132 133.readme :deep(p) { 134 margin-bottom: 1rem; 135} 136 137.readme :deep(a) { 138 @apply underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg transition-colors duration-200; 139} 140.readme :deep(a:hover) { 141 @apply decoration-accent text-accent; 142} 143.readme :deep(a:focus-visible) { 144 @apply decoration-accent text-accent; 145} 146 147.readme :deep(a[target='_blank']:not(:has(img))::after) { 148 /* 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. */ 149 content: '__'; 150 @apply inline i-lucide:external-link rtl-flip ms-1 opacity-50; 151} 152 153.readme :deep(a[href^='#']::after) { 154 /* 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. */ 155 content: '__'; 156 @apply inline i-lucide:link rtl-flip ms-1 opacity-0; 157} 158 159.readme :deep(a[href^='#']:hover::after) { 160 @apply opacity-100; 161} 162 163.readme :deep(code) { 164 @apply font-mono; 165 font-size: 0.875em; 166 background: var(--bg-muted); 167 padding: 0.2em 0.4em; 168 border-radius: 4px; 169 border: 1px solid var(--border); 170} 171 172/* Code blocks - including Shiki output */ 173.readme :deep(pre), 174.readme :deep(.shiki) { 175 border: 1px solid var(--border); 176 border-radius: 8px; 177 padding: 1rem; 178 overflow-x: auto; 179 margin: 1.5rem 0; 180 /* Fix horizontal overflow */ 181 max-width: 100%; 182 box-sizing: border-box; 183} 184 185.readme :deep(.readme-code-block) { 186 @apply bg-bg-subtle; 187 display: block; 188 width: 100%; 189 position: relative; 190} 191 192.readme :deep(.readme-copy-button) { 193 position: absolute; 194 top: 0.4rem; 195 inset-inline-end: 0.4rem; 196 display: inline-flex; 197 align-items: center; 198 justify-content: center; 199 padding: 0.25rem; 200 border-radius: 6px; 201 background: color-mix(in srgb, var(--bg-subtle) 80%, transparent); 202 border: 1px solid var(--border); 203 color: var(--fg-subtle); 204 opacity: 0; 205 transition: 206 opacity 0.2s ease, 207 color 0.2s ease, 208 border-color 0.2s ease; 209} 210 211.readme :deep(.readme-code-block:hover .readme-copy-button), 212.readme :deep(.readme-copy-button:focus-visible) { 213 opacity: 1; 214} 215 216.readme :deep(.readme-copy-button:hover) { 217 color: var(--fg); 218 border-color: var(--border-hover); 219} 220 221.readme :deep(.readme-copy-button > span) { 222 width: 1.05rem; 223 height: 1.05rem; 224 display: inline-block; 225 pointer-events: none; 226} 227 228.readme :deep(pre code), 229.readme :deep(.shiki code) { 230 background: transparent !important; 231 border: none; 232 padding: 0; 233 @apply font-mono; 234 font-size: 0.875rem; 235 color: var(--fg); 236 /* Prevent code from forcing width */ 237 white-space: pre; 238 word-break: normal; 239 overflow-wrap: normal; 240 /* Makes unicode and ascii art work properly */ 241 line-height: 1.25; 242 display: inline-block; 243} 244 245.readme :deep(ul), 246.readme :deep(ol) { 247 margin: 1rem 0; 248 padding-inline-start: 1.5rem; 249} 250 251.readme :deep(ul) { 252 list-style-type: disc; 253} 254 255.readme :deep(ol) { 256 list-style-type: decimal; 257} 258 259.readme :deep(li) { 260 margin-bottom: 0.5rem; 261 display: list-item; 262} 263 264.readme :deep(li::marker) { 265 color: var(--border-hover); 266} 267 268.readme :deep(blockquote) { 269 border-inline-start: 2px solid var(--border); 270 padding-inline-start: 1rem; 271 margin: 1.5rem 0; 272 color: var(--fg-subtle); 273 font-style: italic; 274} 275 276/* GitHub-style callouts/alerts */ 277.readme :deep(blockquote[data-callout]) { 278 border-inline-start-width: 3px; 279 padding: 1rem; 280 padding-inline-start: 1.25rem; 281 background: var(--bg-subtle); 282 font-style: normal; 283 color: var(--fg-subtle); 284 position: relative; 285} 286 287.readme :deep(blockquote[data-callout]::before) { 288 display: block; 289 @apply font-mono; 290 font-size: 0.75rem; 291 font-weight: 500; 292 text-transform: uppercase; 293 letter-spacing: 0.05em; 294 margin-bottom: 0.5rem; 295 padding-inline-start: 1.5rem; 296} 297 298.readme :deep(blockquote[data-callout]::after) { 299 content: ''; 300 width: 1.25rem; 301 height: 1.25rem; 302 position: absolute; 303 top: 1rem; 304 left: 1rem; 305} 306 307.readme :deep(blockquote[data-callout] > p:first-child) { 308 margin-top: 0; 309} 310 311.readme :deep(blockquote[data-callout] > p:last-child) { 312 margin-bottom: 0; 313} 314 315/* Note - blue */ 316.readme :deep(blockquote[data-callout='note']) { 317 border-inline-start-color: var(--syntax-str); 318 background: rgba(59, 130, 246, 0.05); 319} 320.readme :deep(blockquote[data-callout='note']::before) { 321 content: var(--i18n-note, 'Note'); 322 color: #3b82f6; 323} 324.readme :deep(blockquote[data-callout='note']::after) { 325 background-color: #3b82f6; 326 -webkit-mask: icon('i-lucide:info') no-repeat; 327 mask: icon('i-lucide:info') no-repeat; 328} 329 330/* Tip - green */ 331.readme :deep(blockquote[data-callout='tip']) { 332 border-inline-start-color: #22c55e; 333 background: rgba(34, 197, 94, 0.05); 334} 335.readme :deep(blockquote[data-callout='tip']::before) { 336 content: var(--i18n-tip, 'Tip'); 337 color: #22c55e; 338} 339.readme :deep(blockquote[data-callout='tip']::after) { 340 background-color: #22c55e; 341 -webkit-mask: icon('i-lucide:lightbulb') no-repeat; 342 mask: icon('i-lucide:lightbulb') no-repeat; 343} 344 345/* Important - purple */ 346.readme :deep(blockquote[data-callout='important']) { 347 border-inline-start-color: var(--syntax-fn); 348 background: rgba(168, 85, 247, 0.05); 349} 350.readme :deep(blockquote[data-callout='important']::before) { 351 content: var(--i18n-important, 'Important'); 352 color: var(--syntax-fn); 353} 354.readme :deep(blockquote[data-callout='important']::after) { 355 background-color: var(--syntax-fn); 356 -webkit-mask: icon('i-lucide:pin') no-repeat; 357 mask: icon('i-lucide:pin') no-repeat; 358} 359 360/* Warning - yellow/orange */ 361.readme :deep(blockquote[data-callout='warning']) { 362 border-inline-start-color: #eab308; 363 background: rgba(234, 179, 8, 0.05); 364} 365.readme :deep(blockquote[data-callout='warning']::before) { 366 content: var(--i18n-warning, 'Warning'); 367 color: #eab308; 368} 369.readme :deep(blockquote[data-callout='warning']::after) { 370 background-color: #eab308; 371 -webkit-mask: icon('i-lucide:triangle-alert') no-repeat; 372 mask: icon('i-lucide:triangle-alert') no-repeat; 373} 374 375/* Caution - red */ 376.readme :deep(blockquote[data-callout='caution']) { 377 border-inline-start-color: #ef4444; 378 background: rgba(239, 68, 68, 0.05); 379} 380.readme :deep(blockquote[data-callout='caution']::before) { 381 content: var(--i18n-caution, 'Caution'); 382 color: #ef4444; 383} 384.readme :deep(blockquote[data-callout='caution']::after) { 385 background-color: #ef4444; 386 -webkit-mask: icon('i-lucide:circle-alert') no-repeat; 387 mask: icon('i-lucide:circle-alert') no-repeat; 388} 389 390/* Table wrapper for horizontal scroll on mobile */ 391.readme :deep(table) { 392 display: block; 393 width: 100%; 394 overflow-x: auto; 395 border-collapse: collapse; 396 margin: 1.5rem 0; 397 font-size: 0.875rem; 398 word-break: keep-all; 399} 400 401.readme :deep(th), 402.readme :deep(td) { 403 border: 1px solid var(--border); 404 padding: 0.75rem 1rem; 405 text-align: start; 406} 407 408.readme :deep(th) { 409 background: var(--bg-subtle); 410 color: var(--fg); 411 font-weight: 500; 412} 413 414.readme :deep(tr:hover) { 415 background: var(--bg-subtle); 416} 417 418.readme :deep(img) { 419 max-width: 100%; 420 height: revert-layer; 421 display: revert-layer; 422 border-radius: 8px; 423 margin: 1rem 0; 424 position: relative; 425 z-index: 1; 426} 427 428.readme :deep(video) { 429 height: revert-layer; 430 display: revert-layer; 431} 432 433.readme :deep(hr) { 434 border: none; 435 border-top: 1px solid var(--border); 436 margin: 2rem 0; 437} 438 439/* Badge images inline */ 440.readme :deep(p > a > img), 441.readme :deep(p > img) { 442 display: inline-block; 443 margin: 0 0.25rem 0.25rem 0; 444 border-radius: 4px; 445} 446 447/* Screen reader only text */ 448.readme :deep(.sr-only) { 449 position: absolute; 450 width: 1px; 451 height: 1px; 452 padding: 0; 453 margin: -1px; 454 overflow: hidden; 455 clip: rect(0, 0, 0, 0); 456 white-space: nowrap; 457 border-width: 0; 458} 459</style>