Skip to content

Commit 4d8bcea

Browse files
committed
Merge branch 'main' into feat/org-packages-fancy
2 parents 2a10d20 + 7b064eb commit 4d8bcea

39 files changed

Lines changed: 3355 additions & 972 deletions

.github/workflows/lunaria.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ permissions:
1717

1818
jobs:
1919
lunaria-overview:
20-
if: false # temporarily disabled
2120
name: Generate Lunaria Overview
2221
runs-on: ubuntu-latest
2322

@@ -29,7 +28,6 @@ jobs:
2928
# Makes the action clone the entire git history
3029
fetch-depth: 0
3130

32-
- uses: actions/checkout@v6
3331
- run: corepack enable
3432
- uses: actions/setup-node@v6
3533
with:

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
</p>
1010

1111
- [👉 &nbsp;Check it out](https://npmx.dev/)
12+
- [📖 &nbsp;About npmx](https://npmx.dev/about)
1213

1314
## Vision
1415

15-
The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the npm registry &ndash; fast, modern, and accessible. We don't aim to replace the [npmjs.com](https://www.npmjs.com/) registry, just provide a better UI and DX.
16+
The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the npm registry &ndash; fast, modern, and accessible. We don't aim to replace the [npmjs.com](https://www.npmjs.com/) registry, just provide a better UI, DX, and admin experience.
1617

1718
- **Speed first** &ndash; Layout shift, flakiness, slowness is The Worst. Fast searching, filtering, and navigation.
1819
- **URL compatible** &ndash; Replace `npmjs.com` with `xnpmjs.com` or `npmx.dev` in any URL and it just works.
1920
- **Simplicity** &ndash; No noise, cluttered display, or confusing UI. If in doubt: choose simplicity.
21+
- **Admin UI** &ndash; Manage your packages, teams, and organizations from the browser, powered by your local npm CLI.
2022

2123
## Shortcuts
2224

app/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const router = useRouter()
77
// Initialize accent color before hydration to prevent flash
88
initAccentOnPrehydrate()
99
10-
const isHomepage = computed(() => route.path === '/')
10+
const isHomepage = computed(() => route.name === 'index')
1111
1212
useHead({
1313
titleTemplate: titleChunk => {

app/assets/main.css

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ html {
5858
-webkit-font-smoothing: antialiased;
5959
-moz-osx-font-smoothing: grayscale;
6060
text-rendering: optimizeLegibility;
61+
scroll-behavior: smooth;
62+
scroll-padding-top: 5rem; /* Offset for fixed header - otherwise anchor headers are cutted */
63+
}
64+
65+
/* Disable smooth scrolling if user prefers reduced motion */
66+
@media (prefers-reduced-motion: reduce) {
67+
html {
68+
scroll-behavior: auto;
69+
}
6170
}
6271

6372
/*
@@ -76,7 +85,6 @@ body {
7685
background-color: var(--bg);
7786
color: var(--fg);
7887
line-height: 1.6;
79-
padding-bottom: var(--footer-height, 0);
8088
}
8189

8290
/* Default link styling for accessibility on dark background */
@@ -247,7 +255,6 @@ html.light .shiki span {
247255
/* Code blocks - including Shiki output */
248256
.readme-content pre,
249257
.readme-content .shiki {
250-
background: oklch(0.145 0 0) !important;
251258
border: 1px solid var(--border);
252259
border-radius: 8px;
253260
padding: 1rem;
@@ -310,6 +317,7 @@ html.light .shiki span {
310317
background: var(--bg-subtle);
311318
font-style: normal;
312319
color: var(--fg-subtle);
320+
position: relative;
313321
}
314322

315323
.readme-content blockquote[data-callout]::before {
@@ -320,6 +328,15 @@ html.light .shiki span {
320328
text-transform: uppercase;
321329
letter-spacing: 0.05em;
322330
margin-bottom: 0.5rem;
331+
padding-left: 1.5rem;
332+
}
333+
334+
.readme-content blockquote[data-callout]::after {
335+
content: '';
336+
width: 1.25rem;
337+
height: 1.25rem;
338+
position: absolute;
339+
top: 1rem;
323340
}
324341

325342
.readme-content blockquote[data-callout] > p:first-child {
@@ -339,6 +356,11 @@ html.light .shiki span {
339356
content: 'Note';
340357
color: #3b82f6;
341358
}
359+
.readme-content blockquote[data-callout='note']::after {
360+
background-color: #3b82f6;
361+
-webkit-mask: icon('i-lucide-info') no-repeat;
362+
mask: icon('i-lucide-info') no-repeat;
363+
}
342364

343365
/* Tip - green */
344366
.readme-content blockquote[data-callout='tip'] {
@@ -349,6 +371,11 @@ html.light .shiki span {
349371
content: 'Tip';
350372
color: #22c55e;
351373
}
374+
.readme-content blockquote[data-callout='tip']::after {
375+
background-color: #22c55e;
376+
-webkit-mask: icon('i-lucide-lightbulb') no-repeat;
377+
mask: icon('i-lucide-lightbulb') no-repeat;
378+
}
352379

353380
/* Important - purple */
354381
.readme-content blockquote[data-callout='important'] {
@@ -359,6 +386,11 @@ html.light .shiki span {
359386
content: 'Important';
360387
color: var(--syntax-fn);
361388
}
389+
.readme-content blockquote[data-callout='important']::after {
390+
background-color: var(--syntax-fn);
391+
-webkit-mask: icon('i-lucide-pin') no-repeat;
392+
mask: icon('i-lucide-pin') no-repeat;
393+
}
362394

363395
/* Warning - yellow/orange */
364396
.readme-content blockquote[data-callout='warning'] {
@@ -369,6 +401,11 @@ html.light .shiki span {
369401
content: 'Warning';
370402
color: #eab308;
371403
}
404+
.readme-content blockquote[data-callout='warning']::after {
405+
background-color: #eab308;
406+
-webkit-mask: icon('i-lucide-triangle-alert') no-repeat;
407+
mask: icon('i-lucide-triangle-alert') no-repeat;
408+
}
372409

373410
/* Caution - red */
374411
.readme-content blockquote[data-callout='caution'] {
@@ -379,6 +416,11 @@ html.light .shiki span {
379416
content: 'Caution';
380417
color: #ef4444;
381418
}
419+
.readme-content blockquote[data-callout='caution']::after {
420+
background-color: #ef4444;
421+
-webkit-mask: icon('i-lucide-circle-alert') no-repeat;
422+
mask: icon('i-lucide-circle-alert') no-repeat;
423+
}
382424

383425
/* Table wrapper for horizontal scroll on mobile */
384426
.readme-content table {

app/components/AppFooter.vue

Lines changed: 21 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,57 @@
1-
<script setup lang="ts">
2-
const isMounted = shallowRef(false)
3-
const isVisible = shallowRef(false)
4-
const isScrollable = shallowRef(true)
5-
const lastScrollY = shallowRef(0)
6-
const footerRef = useTemplateRef('footerRef')
7-
8-
// Check if CSS scroll-state container queries are supported
9-
// Once this becomes baseline, we can remove the JS scroll handling entirely
10-
const supportsScrollStateQueries = useSupported(() => {
11-
return isMounted.value && CSS.supports('container-type', 'scroll-state')
12-
})
13-
14-
function checkScrollable() {
15-
return document.documentElement.scrollHeight > window.innerHeight
16-
}
17-
18-
function onScroll() {
19-
// Skip JS-based visibility logic if CSS scroll-state queries handle it
20-
if (supportsScrollStateQueries.value) return
21-
22-
const currentY = window.scrollY
23-
const diff = lastScrollY.value - currentY
24-
const nearBottom = currentY + window.innerHeight >= document.documentElement.scrollHeight - 50
25-
26-
// Scrolling UP or near bottom -> show
27-
if (Math.abs(diff) > 10) {
28-
isVisible.value = diff > 0 || nearBottom
29-
lastScrollY.value = currentY
30-
}
31-
32-
// At top -> hide
33-
if (currentY < 100) {
34-
isVisible.value = false
35-
}
36-
37-
// Near bottom -> always show
38-
if (nearBottom) {
39-
isVisible.value = true
40-
}
41-
}
42-
43-
function updateFooterPadding() {
44-
const height = isScrollable.value && footerRef.value ? footerRef.value.offsetHeight : 0
45-
document.documentElement.style.setProperty('--footer-height', `${height}px`)
46-
}
47-
48-
function onResize() {
49-
isScrollable.value = checkScrollable()
50-
updateFooterPadding()
51-
}
52-
53-
useEventListener('scroll', onScroll, { passive: true })
54-
useEventListener('resize', onResize, { passive: true })
55-
56-
onMounted(() => {
57-
nextTick(() => {
58-
lastScrollY.value = window.scrollY
59-
isScrollable.value = checkScrollable()
60-
updateFooterPadding()
61-
// Only apply dynamic classes after mount to avoid hydration mismatch
62-
isMounted.value = true
63-
})
64-
})
65-
</script>
66-
671
<template>
68-
<footer
69-
ref="footerRef"
70-
aria-label="Site footer"
71-
class="border-t border-border bg-bg/90 backdrop-blur-md"
72-
:class="[
73-
// When CSS scroll-state queries are supported, use CSS-only approach
74-
supportsScrollStateQueries
75-
? 'footer-scroll-state'
76-
: // JS-controlled: fixed position, hidden by default, transition only after mount
77-
isScrollable
78-
? [
79-
'fixed bottom-0 left-0 right-0 z-40',
80-
isMounted && 'transition-transform duration-300 ease-out',
81-
isVisible ? 'translate-y-0' : 'translate-y-full',
82-
]
83-
: 'mt-auto',
84-
]"
85-
>
86-
<div class="container py-2 sm:py-6 flex flex-col gap-1 sm:gap-3 text-fg-subtle text-sm">
87-
<div class="flex flex-row items-center justify-between gap-2 sm:gap-4">
2+
<footer class="border-t border-border mt-auto" aria-label="Site footer">
3+
<div class="container py-3 sm:py-8 flex flex-col gap-2 sm:gap-4 text-fg-subtle text-sm">
4+
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 sm:gap-4">
885
<p class="font-mono m-0 hidden sm:block">{{ $t('tagline') }}</p>
89-
<!-- On mobile, show disclaimer here instead of tagline -->
90-
<p class="text-xs text-fg-muted m-0 sm:hidden">{{ $t('non_affiliation_disclaimer') }}</p>
91-
<div class="flex items-center gap-4 sm:gap-6">
6+
<div class="flex items-center gap-3 sm:gap-6">
7+
<NuxtLink
8+
to="/about"
9+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
10+
>
11+
{{ $t('footer.about') }}
12+
</NuxtLink>
9213
<a
9314
href="https://docs.npmx.dev"
9415
target="_blank"
9516
rel="noopener noreferrer"
96-
class="link-subtle font-mono text-xs min-h-11 min-w- flex items-center"
17+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
9718
>
9819
{{ $t('footer.docs') }}
20+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
9921
</a>
10022
<a
10123
href="https://repo.npmx.dev"
10224
target="_blank"
10325
rel="noopener noreferrer"
104-
class="link-subtle font-mono text-xs min-h-11 min-w- flex items-center"
26+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
10527
>
10628
{{ $t('footer.source') }}
29+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
10730
</a>
10831
<a
10932
href="https://social.npmx.dev"
11033
target="_blank"
11134
rel="noopener noreferrer"
112-
class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
35+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
11336
>
11437
{{ $t('footer.social') }}
38+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
11539
</a>
11640
<a
11741
href="https://chat.npmx.dev"
11842
target="_blank"
11943
rel="noopener noreferrer"
120-
class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
44+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
12145
>
12246
{{ $t('footer.chat') }}
47+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
12348
</a>
12449
</div>
12550
</div>
126-
<p class="text-xs text-fg-muted text-center sm:text-left m-0 hidden sm:block">
127-
{{ $t('trademark_disclaimer') }}
51+
<p class="text-xs text-fg-muted text-center sm:text-left m-0">
52+
<span class="sm:hidden">{{ $t('non_affiliation_disclaimer') }}</span>
53+
<span class="hidden sm:inline">{{ $t('trademark_disclaimer') }}</span>
12854
</p>
12955
</div>
13056
</footer>
13157
</template>
132-
133-
<style scoped>
134-
/*
135-
* CSS scroll-state container queries (Chrome 133+)
136-
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/@container#scroll-state_container_descriptors
137-
*
138-
* This provides a pure CSS solution for showing/hiding the footer based on scroll state.
139-
* The JS fallback handles browsers without support.
140-
* Once scroll-state queries become baseline, we can remove the JS scroll handling entirely.
141-
*/
142-
@supports (container-type: scroll-state) {
143-
.footer-scroll-state {
144-
position: fixed;
145-
bottom: 0;
146-
left: 0;
147-
right: 0;
148-
z-index: 40;
149-
/* Hidden by default (translated off-screen) */
150-
transform: translateY(100%);
151-
}
152-
153-
@media (prefers-reduced-motion: no-preference) {
154-
.footer-scroll-state {
155-
transition: transform 0.3s ease-out;
156-
}
157-
}
158-
159-
/* Show footer when user can scroll up (meaning they've scrolled down) */
160-
@container scroll-state(scrollable: top) {
161-
.footer-scroll-state {
162-
transform: translateY(0);
163-
}
164-
}
165-
}
166-
</style>

0 commit comments

Comments
 (0)