Skip to content

Commit c8405ee

Browse files
committed
feat: integrate connector with ui
1 parent fecc8e5 commit c8405ee

21 files changed

Lines changed: 3706 additions & 90 deletions

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ The aim of [npmx.dev](https://npmx.dev) is to provide a better npm package brows
2020

2121
## Features
2222

23+
- **Dark mode by default** - Easy on the eyes, no toggle needed
2324
- **Package browsing** - Fast search, package details, READMEs, versions, dependencies
25+
- **Dependencies view** - Shows regular dependencies and peer dependencies (with optional badges)
2426
- **User profiles** - View any npm user's public packages at `/~username`
2527
- **Organization pages** - Browse org packages at `/org/orgname`
2628
- **Provenance indicators** - Verified build indicators for packages with npm provenance
29+
- **Admin features** - Org/team management, package access controls via local connector
2730

2831
### URL Compatibility
2932

app/app.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ onUnmounted(() => {
7575
class="w-1"
7676
/>
7777

78-
<ul class="flex items-center gap-6">
78+
<ul class="flex items-center gap-4 sm:gap-6">
7979
<li class="flex">
8080
<NuxtLink
8181
to="/search"
@@ -95,6 +95,11 @@ onUnmounted(() => {
9595
<span class="hidden sm:inline">github</span>
9696
</a>
9797
</li>
98+
<li class="flex">
99+
<ClientOnly>
100+
<ConnectorStatus />
101+
</ClientOnly>
102+
</li>
98103
</ul>
99104
</nav>
100105
</header>

app/components/ConnectorModal.vue

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<script setup lang="ts">
2+
const open = defineModel<boolean>('open', { default: false })
3+
4+
const { isConnected, isConnecting, npmUser, error, hasOperations, connect, disconnect } = useConnector()
5+
6+
const tokenInput = ref('')
7+
const portInput = ref('31415')
8+
9+
async function handleConnect() {
10+
const port = Number.parseInt(portInput.value, 10) || 31415
11+
const success = await connect(tokenInput.value.trim(), port)
12+
if (success) {
13+
tokenInput.value = ''
14+
open.value = false
15+
}
16+
}
17+
18+
function handleDisconnect() {
19+
disconnect()
20+
}
21+
22+
// Reset form when modal opens
23+
watch(open, (isOpen) => {
24+
if (isOpen) {
25+
tokenInput.value = ''
26+
}
27+
})
28+
</script>
29+
30+
<template>
31+
<Teleport to="body">
32+
<Transition
33+
enter-active-class="transition-opacity duration-200"
34+
leave-active-class="transition-opacity duration-200"
35+
enter-from-class="opacity-0"
36+
leave-to-class="opacity-0"
37+
>
38+
<div
39+
v-if="open"
40+
class="fixed inset-0 z-50 flex items-center justify-center p-4"
41+
>
42+
<!-- Backdrop -->
43+
<button
44+
type="button"
45+
class="absolute inset-0 bg-black/60 cursor-default"
46+
aria-label="Close modal"
47+
@click="open = false"
48+
/>
49+
50+
<!-- Modal -->
51+
<div
52+
class="relative w-full bg-bg border border-border rounded-lg shadow-xl max-h-[90vh] overflow-y-auto overscroll-contain"
53+
:class="isConnected && hasOperations ? 'max-w-2xl' : 'max-w-md'"
54+
role="dialog"
55+
aria-modal="true"
56+
aria-labelledby="connector-modal-title"
57+
>
58+
<div class="p-6">
59+
<div class="flex items-center justify-between mb-6">
60+
<h2
61+
id="connector-modal-title"
62+
class="font-mono text-lg font-medium"
63+
>
64+
Local Connector
65+
</h2>
66+
<button
67+
type="button"
68+
class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
69+
aria-label="Close"
70+
@click="open = false"
71+
>
72+
<span
73+
class="i-carbon-close block w-5 h-5"
74+
aria-hidden="true"
75+
/>
76+
</button>
77+
</div>
78+
79+
<!-- Connected state -->
80+
<div
81+
v-if="isConnected"
82+
class="space-y-4"
83+
>
84+
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
85+
<span
86+
class="w-3 h-3 rounded-full bg-green-500"
87+
aria-hidden="true"
88+
/>
89+
<div>
90+
<p class="font-mono text-sm text-fg">
91+
Connected
92+
</p>
93+
<p
94+
v-if="npmUser"
95+
class="font-mono text-xs text-fg-muted"
96+
>
97+
Logged in as @{{ npmUser }}
98+
</p>
99+
</div>
100+
</div>
101+
102+
<!-- Operations Queue -->
103+
<OperationsQueue />
104+
105+
<div
106+
v-if="!hasOperations"
107+
class="text-sm text-fg-muted"
108+
>
109+
You can now manage packages, organizations, and teams through the npmx.dev interface.
110+
</div>
111+
112+
<button
113+
type="button"
114+
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
115+
@click="handleDisconnect"
116+
>
117+
Disconnect
118+
</button>
119+
</div>
120+
121+
<!-- Disconnected state -->
122+
<form
123+
v-else
124+
class="space-y-4"
125+
@submit.prevent="handleConnect"
126+
>
127+
<p class="text-sm text-fg-muted">
128+
Run the connector on your machine to enable admin features:
129+
</p>
130+
131+
<div class="p-3 bg-[#0d0d0d] border border-border rounded-lg font-mono text-sm">
132+
<span class="text-fg-subtle">$</span>
133+
<span class="text-fg ml-2">npx npmx-connector</span>
134+
</div>
135+
136+
<p class="text-sm text-fg-muted">
137+
Then paste the token shown in your terminal:
138+
</p>
139+
140+
<div class="space-y-3">
141+
<div>
142+
<label
143+
for="connector-token"
144+
class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5"
145+
>
146+
Token
147+
</label>
148+
<input
149+
id="connector-token"
150+
v-model="tokenInput"
151+
type="password"
152+
name="connector-token"
153+
placeholder="paste token here…"
154+
autocomplete="off"
155+
spellcheck="false"
156+
class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
157+
>
158+
</div>
159+
160+
<details class="text-sm">
161+
<summary class="text-fg-subtle cursor-pointer hover:text-fg-muted transition-colors duration-200">
162+
Advanced options
163+
</summary>
164+
<div class="mt-3">
165+
<label
166+
for="connector-port"
167+
class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5"
168+
>
169+
Port
170+
</label>
171+
<input
172+
id="connector-port"
173+
v-model="portInput"
174+
type="text"
175+
name="connector-port"
176+
inputmode="numeric"
177+
autocomplete="off"
178+
class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
179+
>
180+
</div>
181+
</details>
182+
</div>
183+
184+
<!-- Error message -->
185+
<div
186+
v-if="error"
187+
role="alert"
188+
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
189+
>
190+
{{ error }}
191+
</div>
192+
193+
<button
194+
type="submit"
195+
:disabled="!tokenInput.trim() || isConnecting"
196+
class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
197+
>
198+
{{ isConnecting ? 'Connecting…' : 'Connect' }}
199+
</button>
200+
</form>
201+
</div>
202+
</div>
203+
</div>
204+
</Transition>
205+
</Teleport>
206+
</template>

app/components/ConnectorStatus.vue

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script setup lang="ts">
2+
const { isConnected, isConnecting, npmUser, error, activeOperations, hasPendingOperations } = useConnector()
3+
4+
const showModal = ref(false)
5+
6+
const statusText = computed(() => {
7+
if (isConnecting.value) return 'connecting…'
8+
if (isConnected.value && npmUser.value) return `@${npmUser.value}`
9+
if (isConnected.value) return 'connected'
10+
return 'disconnected'
11+
})
12+
13+
const statusColor = computed(() => {
14+
if (isConnecting.value) return 'bg-yellow-500'
15+
if (isConnected.value) return 'bg-green-500'
16+
return 'bg-fg-subtle'
17+
})
18+
19+
/** Only show count of active (pending/approved/running) operations */
20+
const operationCount = computed(() => activeOperations.value.length)
21+
22+
const ariaLabel = computed(() => {
23+
if (error.value) return error.value
24+
if (isConnecting.value) return 'Connecting to local connector'
25+
if (isConnected.value) return 'Connected to local connector'
26+
return 'Click to connect to local connector'
27+
})
28+
</script>
29+
30+
<template>
31+
<div class="relative">
32+
<button
33+
type="button"
34+
class="flex items-center gap-2 px-3 py-1.5 font-mono text-xs text-fg-muted hover:text-fg bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
35+
:aria-label="ariaLabel"
36+
@click="showModal = true"
37+
>
38+
<span
39+
class="w-2 h-2 rounded-full transition-colors duration-200"
40+
:class="statusColor"
41+
aria-hidden="true"
42+
/>
43+
<span class="hidden sm:inline">{{ statusText }}</span>
44+
<span class="sm:hidden">{{ isConnected ? 'on' : 'off' }}</span>
45+
<!-- Operation count badge (only active operations) -->
46+
<span
47+
v-if="isConnected && operationCount > 0"
48+
class="min-w-[1.25rem] h-5 px-1 flex items-center justify-center font-mono text-xs rounded-full"
49+
:class="hasPendingOperations ? 'bg-yellow-500 text-black' : 'bg-blue-500 text-white'"
50+
aria-hidden="true"
51+
>
52+
{{ operationCount }}
53+
</span>
54+
</button>
55+
56+
<ConnectorModal
57+
v-model:open="showModal"
58+
/>
59+
</div>
60+
</template>

0 commit comments

Comments
 (0)