forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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>