Skip to content

Commit 003eaa9

Browse files
committed
Merge branch 'main' of github.com:npmx-dev/npmx.dev into feat/version-selection
2 parents b762ce8 + 18bcb8a commit 003eaa9

File tree

110 files changed

+5863
-783
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+5863
-783
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ jobs:
3333

3434
- uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c
3535
name: 🟧 Install pnpm
36-
# pnpm cache skipped deliberately as the project is not actually installed here
36+
with:
37+
cache: true
38+
39+
- name: 📦 Install dependencies (root only, no scripts)
40+
run: pnpm install --filter . --ignore-scripts
3741

3842
- name: 🔠 Lint project
39-
run: node scripts/lint.ts
43+
run: pnpm lint
4044

4145
types:
4246
name: 💪 Type check

.oxlintrc.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"$schema": "https://unpkg.com/oxlint/configuration_schema.json",
33
"plugins": ["unicorn", "typescript", "oxc", "vue", "vitest"],
4+
"jsPlugins": ["@e18e/eslint-plugin"],
45
"categories": {
56
"correctness": "error",
67
"suspicious": "warn",
@@ -11,8 +12,27 @@
1112
"no-await-in-loop": "off",
1213
"unicorn/no-array-sort": "off",
1314
"no-restricted-globals": "error",
14-
"typescript/consistent-type-imports": "error"
15+
"typescript/consistent-type-imports": "error",
16+
"e18e/prefer-array-from-map": "error",
17+
"e18e/prefer-timer-args": "error",
18+
"e18e/prefer-date-now": "error",
19+
"e18e/prefer-regex-test": "error",
20+
"e18e/prefer-array-some": "error"
1521
},
22+
"overrides": [
23+
{
24+
"files": [
25+
"server/**/*",
26+
"cli/**/*",
27+
"scripts/**/*",
28+
"modules/**/*",
29+
"app/components/OgImage/*"
30+
],
31+
"rules": {
32+
"no-console": "off"
33+
}
34+
}
35+
],
1636
"ignorePatterns": [
1737
".output/**",
1838
".data/**",

app/components/AppFooter.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ const isHome = computed(() => route.name === 'index')
1818
<NuxtLink to="/about" class="link-subtle font-mono text-xs flex items-center">
1919
{{ $t('footer.about') }}
2020
</NuxtLink>
21+
<NuxtLink
22+
to="/privacy"
23+
class="link-subtle font-mono text-xs min-h-11 flex items-center gap-1 lowercase"
24+
>
25+
{{ $t('privacy_policy.title') }}
26+
</NuxtLink>
2127
<a
2228
href="https://docs.npmx.dev"
2329
target="_blank"

app/components/AppHeader.vue

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ onKeyStroke(
109109
to="/"
110110
:aria-label="$t('header.home')"
111111
dir="ltr"
112-
class="inline-flex items-center gap-2 header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 rounded"
112+
class="inline-flex items-center gap-1 header-logo font-mono text-lg font-medium text-fg hover:text-fg/90 transition-colors duration-200 rounded"
113113
>
114114
<AppLogo class="w-8 h-8 rounded-lg" />
115115
<span>npmx</span>
@@ -185,19 +185,14 @@ onKeyStroke(
185185
<HeaderAccountMenu />
186186
</div>
187187

188-
<!-- Mobile: Menu button (always visible, toggles menu) -->
188+
<!-- Mobile: Menu button (always visible, click to open menu) -->
189189
<button
190190
type="button"
191191
class="sm:hidden flex items-center p-2 -m-2 text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-accent/70 rounded"
192-
:aria-label="showMobileMenu ? $t('common.close') : $t('nav.open_menu')"
193-
:aria-expanded="showMobileMenu"
194-
@click="showMobileMenu = !showMobileMenu"
192+
:aria-label="$t('nav.open_menu')"
193+
@click="showMobileMenu = true"
195194
>
196-
<span
197-
class="w-6 h-6 inline-block"
198-
:class="showMobileMenu ? 'i-carbon:close' : 'i-carbon:menu'"
199-
aria-hidden="true"
200-
/>
195+
<span class="w-6 h-6 inline-block i-carbon:menu" aria-hidden="true" />
201196
</button>
202197
</div>
203198
</nav>

app/components/AppLogo.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defineProps<{
1515
>
1616
<title>{{ $t('alt_logo') }}</title>
1717
<rect fill="var(--bg)" width="512" height="512" rx="64" />
18-
<rect fill="var(--fg)" x="110" y="310" width="60" height="60" />
18+
<rect fill="currentColor" x="110" y="310" width="60" height="60" />
1919
<text
2020
fill="var(--accent)"
2121
x="320"

app/components/CollapsibleSection.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ useHead({
125125

126126
<div
127127
:id="contentId"
128-
class="grid ms-6 transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden"
128+
class="grid ms-6 grid-rows-[1fr] transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden"
129129
:inert="!isOpen"
130130
>
131-
<div class="min-h-0 min-w-0 p-1">
131+
<div class="min-h-0 min-w-0">
132132
<slot />
133133
</div>
134134
</div>

app/components/Compare/ComparisonGrid.vue

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,101 @@
11
<script setup lang="ts">
2-
defineProps<{
3-
/** Number of columns (2-4) */
4-
columns: number
5-
/** Column headers (package names or version numbers) */
6-
headers: string[]
2+
import type { ModuleReplacement } from 'module-replacements'
3+
4+
export interface ComparisonGridColumn {
5+
/** Display text (e.g. "lodash@4.17.21") */
6+
header: string
7+
/** Module replacement data for this package (if available) */
8+
replacement?: ModuleReplacement | null
9+
}
10+
11+
const props = defineProps<{
12+
/** Column definitions for each package being compared */
13+
columns: ComparisonGridColumn[]
14+
/** Whether to show the "no dependency" baseline as the last column */
15+
showNoDependency?: boolean
716
}>()
17+
18+
/** Total column count including the optional no-dep column */
19+
const totalColumns = computed(() => props.columns.length + (props.showNoDependency ? 1 : 0))
20+
21+
/** Compute plain-text tooltip for a replacement column */
22+
function getReplacementTooltip(col: ComparisonGridColumn): string {
23+
if (!col.replacement) return ''
24+
25+
return [$t('package.replacement.title'), $t('package.replacement.learn_more_above')].join(' ')
26+
}
827
</script>
928

1029
<template>
1130
<div class="overflow-x-auto">
1231
<div
1332
class="comparison-grid"
14-
:class="[columns === 4 ? 'min-w-[800px]' : 'min-w-[600px]', `columns-${columns}`]"
15-
:style="{ '--columns': columns }"
33+
:class="[totalColumns === 4 ? 'min-w-[800px]' : 'min-w-[600px]', `columns-${totalColumns}`]"
34+
:style="{ '--columns': totalColumns }"
1635
>
1736
<!-- Header row -->
1837
<div class="comparison-header">
1938
<div class="comparison-label" />
39+
40+
<!-- Package columns -->
2041
<div
21-
v-for="(header, index) in headers"
22-
:key="index"
42+
v-for="col in columns"
43+
:key="col.header"
2344
class="comparison-cell comparison-cell-header"
2445
>
25-
<NuxtLink
26-
:to="`/package/${header}`"
27-
class="link-subtle font-mono text-sm font-medium text-fg truncate"
28-
:title="header"
46+
<span class="inline-flex items-center gap-1.5 truncate">
47+
<NuxtLink
48+
:to="`/package/${col.header}`"
49+
class="link-subtle font-mono text-sm font-medium text-fg truncate"
50+
:title="col.header"
51+
>
52+
{{ col.header }}
53+
</NuxtLink>
54+
<TooltipApp v-if="col.replacement" :text="getReplacementTooltip(col)" position="bottom">
55+
<span
56+
class="i-carbon:idea w-3.5 h-3.5 text-amber-500 shrink-0 cursor-help"
57+
role="img"
58+
:aria-label="$t('package.replacement.title')"
59+
/>
60+
</TooltipApp>
61+
</span>
62+
</div>
63+
64+
<!-- "No dep" column (always last) -->
65+
<div
66+
v-if="showNoDependency"
67+
class="comparison-cell comparison-cell-header comparison-cell-nodep"
68+
>
69+
<span
70+
class="inline-flex items-center gap-1.5 text-sm font-medium text-accent italic truncate"
2971
>
30-
{{ header }}
31-
</NuxtLink>
72+
{{ $t('compare.no_dependency.label') }}
73+
<TooltipApp interactive position="bottom">
74+
<span
75+
class="i-carbon:idea w-3.5 h-3.5 text-amber-500 shrink-0 cursor-help"
76+
role="img"
77+
:aria-label="$t('compare.no_dependency.tooltip_title')"
78+
/>
79+
<template #content>
80+
<p class="text-sm font-medium text-fg mb-1">
81+
{{ $t('compare.no_dependency.tooltip_title') }}
82+
</p>
83+
<p class="text-xs text-fg-muted">
84+
<i18n-t keypath="compare.no_dependency.tooltip_description" tag="span">
85+
<template #link>
86+
<a
87+
href="https://e18e.dev/docs/replacements/"
88+
target="_blank"
89+
rel="noopener noreferrer"
90+
class="text-accent hover:underline"
91+
>{{ $t('compare.no_dependency.e18e_community') }}</a
92+
>
93+
</template>
94+
</i18n-t>
95+
</p>
96+
</template>
97+
</TooltipApp>
98+
</span>
3299
</div>
33100
</div>
34101

@@ -72,6 +139,16 @@ defineProps<{
72139
text-align: center;
73140
}
74141
142+
/* "No dep" column styling */
143+
.comparison-header > .comparison-cell-header.comparison-cell-nodep {
144+
background: linear-gradient(
145+
135deg,
146+
var(--color-bg-subtle) 0%,
147+
color-mix(in srgb, var(--color-accent) 8%, var(--color-bg-subtle)) 100%
148+
);
149+
border-bottom-color: color-mix(in srgb, var(--color-accent) 30%, var(--color-border));
150+
}
151+
75152
/* First header cell rounded top-start */
76153
.comparison-header > .comparison-cell-header:first-of-type {
77154
border-start-start-radius: 0.5rem;

app/components/Compare/PackageSelector.vue

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script setup lang="ts">
2+
import { NO_DEPENDENCY_ID } from '~/composables/usePackageComparison'
3+
24
const packages = defineModel<string[]>({ required: true })
35
46
const props = defineProps<{
@@ -17,6 +19,29 @@ const { data: searchData, status } = useNpmSearch(inputValue, { size: 15 })
1719
1820
const isSearching = computed(() => status.value === 'pending')
1921
22+
// Trigger strings for "What Would James Do?" typeahead Easter egg
23+
// Intentionally not localized
24+
const EASTER_EGG_TRIGGERS = new Set([
25+
'no dep',
26+
'none',
27+
'vanilla',
28+
'diy',
29+
'zero',
30+
'nothing',
31+
'0',
32+
"don't",
33+
'native',
34+
'use the platform',
35+
])
36+
37+
// Check if "no dependency" option should show in typeahead
38+
const showNoDependencyOption = computed(() => {
39+
if (packages.value.includes(NO_DEPENDENCY_ID)) return false
40+
const input = inputValue.value.toLowerCase().trim()
41+
if (!input) return false
42+
return EASTER_EGG_TRIGGERS.has(input)
43+
})
44+
2045
// Filter out already selected packages
2146
const filteredResults = computed(() => {
2247
if (!searchData.value?.objects) return []
@@ -32,7 +57,16 @@ function addPackage(name: string) {
3257
if (packages.value.length >= maxPackages.value) return
3358
if (packages.value.includes(name)) return
3459
35-
packages.value = [...packages.value, name]
60+
// Keep NO_DEPENDENCY_ID always last
61+
if (name === NO_DEPENDENCY_ID) {
62+
packages.value = [...packages.value, name]
63+
} else if (packages.value.includes(NO_DEPENDENCY_ID)) {
64+
// Insert before the no-dep entry
65+
const withoutNoDep = packages.value.filter(p => p !== NO_DEPENDENCY_ID)
66+
packages.value = [...withoutNoDep, name, NO_DEPENDENCY_ID]
67+
} else {
68+
packages.value = [...packages.value, name]
69+
}
3670
inputValue.value = ''
3771
}
3872
@@ -63,16 +97,28 @@ function handleBlur() {
6397
:key="pkg"
6498
class="inline-flex items-center gap-2 px-3 py-1.5 bg-bg-subtle border border-border rounded-md"
6599
>
100+
<!-- No dependency display -->
101+
<template v-if="pkg === NO_DEPENDENCY_ID">
102+
<span class="text-sm text-accent italic flex items-center gap-1.5">
103+
<span class="i-carbon:clean w-3.5 h-3.5" aria-hidden="true" />
104+
{{ $t('compare.no_dependency.label') }}
105+
</span>
106+
</template>
66107
<NuxtLink
108+
v-else
67109
:to="`/package/${pkg}`"
68110
class="font-mono text-sm text-fg hover:text-accent transition-colors"
69111
>
70112
{{ pkg }}
71113
</NuxtLink>
72114
<button
73115
type="button"
74-
class="text-fg-subtle hover:text-fg transition-colors focus-visible:outline-accent/70 rounded"
75-
:aria-label="$t('compare.selector.remove_package', { package: pkg })"
116+
class="text-fg-subtle hover:text-fg transition-colors rounded"
117+
:aria-label="
118+
$t('compare.selector.remove_package', {
119+
package: pkg === NO_DEPENDENCY_ID ? $t('compare.no_dependency.label') : pkg,
120+
})
121+
"
76122
@click="removePackage(pkg)"
77123
>
78124
<span class="i-carbon:close flex items-center w-3.5 h-3.5" aria-hidden="true" />
@@ -118,17 +164,36 @@ function handleBlur() {
118164
leave-to-class="opacity-0"
119165
>
120166
<div
121-
v-if="isInputFocused && (filteredResults.length > 0 || isSearching)"
167+
v-if="
168+
isInputFocused && (filteredResults.length > 0 || isSearching || showNoDependencyOption)
169+
"
122170
class="absolute top-full inset-x-0 mt-1 bg-bg-elevated border border-border rounded-lg shadow-lg z-50 max-h-64 overflow-y-auto"
123171
>
172+
<!-- No dependency option (easter egg with James) -->
173+
<button
174+
v-if="showNoDependencyOption"
175+
type="button"
176+
class="w-full text-start px-4 py-2.5 hover:bg-bg-muted transition-colors focus-visible:outline-none focus-visible:bg-bg-muted border-b border-border/50"
177+
:aria-label="$t('compare.no_dependency.add_column')"
178+
@click="addPackage(NO_DEPENDENCY_ID)"
179+
>
180+
<div class="text-sm text-accent italic flex items-center gap-2">
181+
<span class="i-carbon:clean w-4 h-4" aria-hidden="true" />
182+
{{ $t('compare.no_dependency.typeahead_title') }}
183+
</div>
184+
<div class="text-xs text-fg-muted truncate mt-0.5">
185+
{{ $t('compare.no_dependency.typeahead_description') }}
186+
</div>
187+
</button>
188+
124189
<div v-if="isSearching" class="px-4 py-3 text-sm text-fg-muted">
125190
{{ $t('compare.selector.searching') }}
126191
</div>
127192
<button
128193
v-for="result in filteredResults"
129194
:key="result.name"
130195
type="button"
131-
class="w-full text-left px-4 py-2.5 hover:bg-bg-muted transition-colors focus-visible:outline-none focus-visible:bg-bg-muted"
196+
class="w-full text-start px-4 py-2.5 hover:bg-bg-muted transition-colors focus-visible:outline-none focus-visible:bg-bg-muted"
132197
@click="addPackage(result.name)"
133198
>
134199
<div class="font-mono text-sm text-fg">{{ result.name }}</div>
@@ -142,7 +207,12 @@ function handleBlur() {
142207

143208
<!-- Hint -->
144209
<p class="text-xs text-fg-subtle">
145-
{{ $t('compare.selector.packages_selected', { count: packages.length, max: maxPackages }) }}
210+
{{
211+
$t('compare.selector.packages_selected', {
212+
count: packages.length,
213+
max: maxPackages,
214+
})
215+
}}
146216
<span v-if="packages.length < 2">{{ $t('compare.selector.add_hint') }}</span>
147217
</p>
148218
</div>

0 commit comments

Comments
 (0)