Skip to content

Commit 1299e68

Browse files
committed
2 parents 3a30c0a + a6d2037 commit 1299e68

47 files changed

Lines changed: 2885 additions & 478 deletions

Some content is hidden

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

app/components/AppHeader.vue

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ withDefaults(
2626
<span v-else class="w-1" />
2727

2828
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
29-
<li class="flex">
29+
<li class="flex items-center">
3030
<NuxtLink
3131
to="/search"
3232
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
@@ -41,17 +41,23 @@ withDefaults(
4141
</kbd>
4242
</NuxtLink>
4343
</li>
44-
<li v-if="showConnector" class="flex">
44+
<li class="flex items-center">
45+
<ClientOnly>
46+
<SettingsMenu />
47+
</ClientOnly>
48+
</li>
49+
<li v-if="showConnector" class="flex items-center">
4550
<ConnectorStatus />
4651
</li>
47-
<li v-else class="flex">
52+
<li v-else class="flex items-center">
4853
<a
4954
href="https://github.com/npmx-dev/npmx.dev"
5055
rel="noopener noreferrer"
5156
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
57+
aria-label="GitHub"
5258
>
53-
<span class="i-carbon-logo-github w-4 h-4" />
54-
<span class="hidden sm:inline">github</span>
59+
<span class="i-carbon-logo-github w-4 h-4" aria-hidden="true" />
60+
<span class="hidden sm:inline" aria-hidden="true">github</span>
5561
</a>
5662
</li>
5763
</ul>

app/components/CodeFileTree.vue

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,18 @@ function isNodeActive(node: PackageFileTree): boolean {
1818
return false
1919
}
2020
21-
// State for expanded directories
22-
const expandedDirs = ref<Set<string>>(new Set())
21+
const { toggleDir, isExpanded, autoExpandAncestors } = useFileTreeState(props.baseUrl)
2322
2423
// Auto-expand directories in the current path
2524
watch(
2625
() => props.currentPath,
2726
path => {
28-
if (!path) return
29-
const parts = path.split('/')
30-
for (let i = 1; i <= parts.length; i++) {
31-
expandedDirs.value.add(parts.slice(0, i).join('/'))
27+
if (path) {
28+
autoExpandAncestors(path)
3229
}
3330
},
3431
{ immediate: true },
3532
)
36-
37-
function toggleDir(path: string) {
38-
if (expandedDirs.value.has(path)) {
39-
expandedDirs.value.delete(path)
40-
} else {
41-
expandedDirs.value.add(path)
42-
}
43-
}
44-
45-
function isExpanded(path: string): boolean {
46-
return expandedDirs.value.has(path)
47-
}
4833
</script>
4934

5035
<template>

app/components/ConnectorStatus.client.vue

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
<script setup lang="ts">
2-
const { isConnected, isConnecting, npmUser, error, activeOperations, hasPendingOperations } =
3-
useConnector()
2+
const {
3+
isConnected,
4+
isConnecting,
5+
npmUser,
6+
avatar,
7+
error,
8+
activeOperations,
9+
hasPendingOperations,
10+
} = useConnector()
411
512
const showModal = shallowRef(false)
613
const showTooltip = shallowRef(false)
@@ -41,8 +48,16 @@ const ariaLabel = computed(() => {
4148
@focus="showTooltip = true"
4249
@blur="showTooltip = false"
4350
>
44-
<!-- Status dot -->
51+
<!-- Avatar (when connected with avatar) -->
52+
<img
53+
v-if="isConnected && avatar"
54+
:src="avatar"
55+
:alt="`${npmUser}'s avatar`"
56+
class="w-6 h-6 rounded-full"
57+
/>
58+
<!-- Status dot (when not connected or no avatar) -->
4559
<span
60+
v-else
4661
class="w-2.5 h-2.5 rounded-full transition-colors duration-200"
4762
:class="statusColor"
4863
aria-hidden="true"

app/components/DateTime.vue

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<script setup lang="ts">
2+
/**
3+
* DateTime component that wraps NuxtTime with settings-aware relative date support.
4+
* Uses the global settings to determine whether to show relative or absolute dates.
5+
*
6+
* Note: When relativeDates setting is enabled, the component switches between
7+
* relative and absolute display based on user preference. The title attribute
8+
* always shows the full date for accessibility.
9+
*/
10+
const props = withDefaults(
11+
defineProps<{
12+
/** The datetime value (ISO string or Date) */
13+
datetime: string | Date
14+
/** Override title (defaults to datetime) */
15+
title?: string
16+
/** Date style for absolute display */
17+
dateStyle?: 'full' | 'long' | 'medium' | 'short'
18+
/** Individual date parts for absolute display (alternative to dateStyle) */
19+
year?: 'numeric' | '2-digit'
20+
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
21+
day?: 'numeric' | '2-digit'
22+
}>(),
23+
{
24+
title: undefined,
25+
dateStyle: undefined,
26+
year: undefined,
27+
month: undefined,
28+
day: undefined,
29+
},
30+
)
31+
32+
const relativeDates = useRelativeDates()
33+
34+
// Compute the title - always show full date for accessibility
35+
const titleValue = computed(() => {
36+
if (props.title) return props.title
37+
if (typeof props.datetime === 'string') return props.datetime
38+
return props.datetime.toISOString()
39+
})
40+
</script>
41+
42+
<template>
43+
<ClientOnly>
44+
<NuxtTime v-if="relativeDates" :datetime="datetime" :title="titleValue" relative />
45+
<NuxtTime
46+
v-else
47+
:datetime="datetime"
48+
:title="titleValue"
49+
:date-style="dateStyle"
50+
:year="year"
51+
:month="month"
52+
:day="day"
53+
/>
54+
<template #fallback>
55+
<NuxtTime
56+
:datetime="datetime"
57+
:title="titleValue"
58+
:date-style="dateStyle"
59+
:year="year"
60+
:month="month"
61+
:day="day"
62+
/>
63+
</template>
64+
</ClientOnly>
65+
</template>

app/components/LicenseDisplay.vue

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import { parseLicenseExpression } from '#shared/utils/spdx'
3+
4+
const props = defineProps<{
5+
license: string
6+
}>()
7+
8+
const tokens = computed(() => parseLicenseExpression(props.license))
9+
10+
const hasAnyValidLicense = computed(() => tokens.value.some(t => t.type === 'license' && t.url))
11+
</script>
12+
13+
<template>
14+
<span class="inline-flex items-baseline gap-x-1.5 flex-wrap gap-y-0.5">
15+
<template v-for="(token, i) in tokens" :key="i">
16+
<a
17+
v-if="token.type === 'license' && token.url"
18+
:href="token.url"
19+
target="_blank"
20+
rel="noopener noreferrer"
21+
class="link-subtle"
22+
title="View license text on SPDX"
23+
>
24+
{{ token.value }}
25+
</a>
26+
<span v-else-if="token.type === 'license'">{{ token.value }}</span>
27+
<span v-else-if="token.type === 'operator'" class="text-[0.65em]">{{ token.value }}</span>
28+
</template>
29+
<span
30+
v-if="hasAnyValidLicense"
31+
class="i-carbon-scales w-3.5 h-3.5 text-fg-subtle flex-shrink-0 inline-block self-center"
32+
aria-hidden="true"
33+
/>
34+
</span>
35+
</template>

app/components/MarkdownText.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script setup lang="ts">
22
const props = defineProps<{
33
text: string
4+
/** When true, renders link text without the anchor tag (useful when inside another link) */
5+
plain?: boolean
46
}>()
57
68
// Escape HTML to prevent XSS
@@ -34,6 +36,23 @@ function parseMarkdown(text: string): string {
3436
// Strikethrough: ~~text~~
3537
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>')
3638
39+
// Links: [text](url) - only allow https, mailto
40+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
41+
// In plain mode, just render the link text without the anchor
42+
if (props.plain) {
43+
return text
44+
}
45+
const decodedUrl = url.replace(/&amp;/g, '&')
46+
try {
47+
const { protocol, href } = new URL(decodedUrl)
48+
if (['https:', 'mailto:'].includes(protocol)) {
49+
const safeUrl = href.replace(/"/g, '&quot;')
50+
return `<a href="${safeUrl}" rel="nofollow noreferrer noopener" target="_blank">${text}</a>`
51+
}
52+
} catch {}
53+
return `${text} (${url})`
54+
})
55+
3756
return html
3857
}
3958

app/components/OperationsQueue.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function getStatusIcon(status: string): string {
9292
case 'approved':
9393
return 'i-carbon-checkmark'
9494
case 'running':
95-
return 'i-carbon-rotate'
95+
return 'i-carbon-rotate-180'
9696
case 'completed':
9797
return 'i-carbon-checkmark-filled'
9898
case 'failed':

app/components/OrgMembersPanel.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ watch(lastExecutionTime, () => {
387387
<!-- Loading state -->
388388
<div v-if="isLoading && memberList.length === 0" class="p-8 text-center">
389389
<span
390-
class="i-carbon-rotate block w-5 h-5 text-fg-muted animate-spin mx-auto"
390+
class="i-carbon-rotate-180 block w-5 h-5 text-fg-muted animate-spin mx-auto"
391391
aria-hidden="true"
392392
/>
393393
<p class="font-mono text-sm text-fg-muted mt-2">Loading members…</p>

app/components/OrgTeamsPanel.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ watch(lastExecutionTime, () => {
327327
<!-- Loading state -->
328328
<div v-if="isLoadingTeams && teams.length === 0" class="p-8 text-center">
329329
<span
330-
class="i-carbon-rotate block w-5 h-5 text-fg-muted animate-spin mx-auto"
330+
class="i-carbon-rotate-180 block w-5 h-5 text-fg-muted animate-spin mx-auto"
331331
aria-hidden="true"
332332
/>
333333
<p class="font-mono text-sm text-fg-muted mt-2">Loading teams…</p>
@@ -382,7 +382,7 @@ watch(lastExecutionTime, () => {
382382
</span>
383383
<span
384384
v-if="isLoadingUsers[teamName]"
385-
class="i-carbon-rotate w-3 h-3 text-fg-muted animate-spin"
385+
class="i-carbon-rotate-180 w-3 h-3 text-fg-muted animate-spin"
386386
aria-hidden="true"
387387
/>
388388
</button>

app/components/PackageAccessControls.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ watch(
173173
<!-- Loading state -->
174174
<div v-if="isLoadingCollaborators && collaboratorList.length === 0" class="py-4 text-center">
175175
<span
176-
class="i-carbon-rotate block w-4 h-4 text-fg-muted animate-spin mx-auto"
176+
class="i-carbon-rotate-180 block w-4 h-4 text-fg-muted animate-spin mx-auto"
177177
aria-hidden="true"
178178
/>
179179
</div>

0 commit comments

Comments
 (0)