Skip to content

Commit d303dd1

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor/modal-a11y
2 parents 5e67d2d + 5b57d2c commit d303dd1

86 files changed

Lines changed: 1276 additions & 734 deletions

File tree

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,4 @@ jobs:
139139
run: pnpm install
140140

141141
- name: 🔍 Check for unused code
142-
run: pnpm knip:production
142+
run: pnpm knip

.vscode/extensions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"recommendations": ["oxc.oxc-vscode", "Vue.volar", "lokalise.i18n-ally"]
2+
"recommendations": ["oxc.oxc-vscode", "Vue.volar", "lokalise.i18n-ally", "antfu.unocss"]
33
}

app/app.vue

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import type { Directions } from '@nuxtjs/i18n'
33
import { useEventListener } from '@vueuse/core'
4+
import { isEditableElement } from '~/utils/input'
45
56
const route = useRoute()
67
const router = useRouter()
@@ -39,12 +40,7 @@ if (import.meta.server) {
3940
// "/" focuses search or navigates to search page
4041
// "?" highlights all keyboard shortcut elements
4142
function handleGlobalKeydown(e: KeyboardEvent) {
42-
const target = e.target as HTMLElement
43-
44-
const isEditableTarget =
45-
target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
46-
47-
if (isEditableTarget) return
43+
if (isEditableElement(e.target)) return
4844
4945
if (e.key === '/') {
5046
e.preventDefault()

app/components/AppHeader.vue

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script setup lang="ts">
2+
import { isEditableElement } from '~/utils/input'
3+
24
withDefaults(
35
defineProps<{
46
showLogo?: boolean
@@ -20,6 +22,7 @@ const isSearchExpandedManually = shallowRef(false)
2022
const searchBoxRef = shallowRef<{ focus: () => void } | null>(null)
2123
2224
// On search page, always show search expanded on mobile
25+
const isOnHomePage = computed(() => route.name === 'index')
2326
const isOnSearchPage = computed(() => route.name === 'search')
2427
const isSearchExpanded = computed(() => isOnSearchPage.value || isSearchExpandedManually.value)
2528
@@ -61,28 +64,38 @@ function handleSearchFocus() {
6164
onKeyStroke(
6265
',',
6366
e => {
64-
// Don't trigger if user is typing in an input
65-
const target = e.target as HTMLElement
66-
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
67-
return
68-
}
67+
if (isEditableElement(e.target)) return
6968
7069
e.preventDefault()
7170
navigateTo('/settings')
7271
},
7372
{ dedupe: true },
7473
)
74+
75+
onKeyStroke(
76+
'c',
77+
e => {
78+
// Allow more specific handlers to take precedence
79+
if (e.defaultPrevented) return
80+
if (isEditableElement(e.target)) return
81+
82+
e.preventDefault()
83+
navigateTo('/compare')
84+
},
85+
{ dedupe: true },
86+
)
7587
</script>
7688

7789
<template>
7890
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
7991
<nav
8092
:aria-label="$t('nav.main_navigation')"
81-
class="container min-h-14 flex items-center justify-between gap-2"
93+
class="container min-h-14 flex items-center gap-2"
94+
:class="isOnHomePage ? 'justify-end' : 'justify-between'"
8295
>
8396
<!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
8497
<button
85-
v-if="!isSearchExpanded"
98+
v-if="!isSearchExpanded && !isOnHomePage"
8699
type="button"
87100
class="sm:hidden flex-shrink-0 inline-flex items-center gap-2 font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
88101
:aria-label="$t('nav.tap_to_search')"
@@ -156,10 +169,16 @@ onKeyStroke(
156169
<!-- Desktop: Compare link -->
157170
<NuxtLink
158171
to="/compare"
159-
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
172+
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
173+
aria-keyshortcuts="c"
160174
>
161-
<span class="i-carbon:compare w-4 h-4" aria-hidden="true" />
162175
{{ $t('nav.compare') }}
176+
<kbd
177+
class="inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
178+
aria-hidden="true"
179+
>
180+
c
181+
</kbd>
163182
</NuxtLink>
164183

165184
<!-- Desktop: Settings link -->

app/components/CollapsibleSection.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ useHead({
123123
:id="contentId"
124124
class="grid ms-6 transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden"
125125
>
126-
<div class="min-h-0">
126+
<div class="min-h-0 min-w-0">
127127
<slot />
128128
</div>
129129
</div>

app/components/MarkdownText.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script setup lang="ts">
2+
import { decodeHtmlEntities } from '~/utils/formatters'
3+
24
const props = defineProps<{
35
text: string
46
/** When true, renders link text without the anchor tag (useful when inside another link) */
@@ -21,8 +23,11 @@ function stripMarkdownImages(text: string): string {
2123
2224
// Strip HTML tags and escape remaining HTML to prevent XSS
2325
function stripAndEscapeHtml(text: string): string {
24-
// First strip markdown image badges
25-
let stripped = stripMarkdownImages(text)
26+
// First decode any HTML entities in the input
27+
let stripped = decodeHtmlEntities(text)
28+
29+
// Then strip markdown image badges
30+
stripped = stripMarkdownImages(stripped)
2631
2732
// Then strip actual HTML tags (keep their text content)
2833
// Only match tags that start with a letter or / (to avoid matching things like "a < b > c")

app/components/OgImage/Package.vue

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<script setup lang="ts">
2-
withDefaults(
2+
import { computed, toRefs } from 'vue'
3+
4+
const props = withDefaults(
35
defineProps<{
46
name: string
57
version: string
8+
stars: number
69
downloads?: string
710
license?: string
811
primaryColor?: string
@@ -13,6 +16,15 @@ withDefaults(
1316
primaryColor: '#60a5fa',
1417
},
1518
)
19+
20+
const { name, version, stars, downloads, license, primaryColor } = toRefs(props)
21+
22+
const formattedStars = computed(() =>
23+
Intl.NumberFormat('en', {
24+
notation: 'compact',
25+
maximumFractionDigits: 1,
26+
}).format(stars.value),
27+
)
1628
</script>
1729

1830
<template>
@@ -88,6 +100,18 @@ withDefaults(
88100
</span>
89101
</span>
90102
<span v-if="license"> • {{ license }}</span>
103+
<span class="flex items-center gap-2">
104+
<span>•</span>
105+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32px" height="32px">
106+
<path
107+
fill="currentColor"
108+
d="m16 6.52l2.76 5.58l.46 1l1 .15l6.16.89l-4.38 4.3l-.75.73l.18 1l1.05 6.13l-5.51-2.89L16 23l-.93.49l-5.51 2.85l1-6.13l.18-1l-.74-.77l-4.42-4.35l6.16-.89l1-.15l.46-1zM16 2l-4.55 9.22l-10.17 1.47l7.36 7.18L6.9 30l9.1-4.78L25.1 30l-1.74-10.13l7.36-7.17l-10.17-1.48Z"
109+
/>
110+
</svg>
111+
<span>
112+
{{ formattedStars }}
113+
</span>
114+
</span>
91115
</div>
92116
</div>
93117

app/components/PackageCard.vue

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const props = defineProps<{
77
/** Whether to show the publisher username */
88
showPublisher?: boolean
99
prefetch?: boolean
10-
selected?: boolean
1110
index?: number
1211
/** Search query for highlighting exact matches */
1312
searchQuery?: string
@@ -20,17 +19,12 @@ const isExactMatch = computed(() => {
2019
const name = props.result.package.name.toLowerCase()
2120
return query === name
2221
})
23-
24-
const emit = defineEmits<{
25-
focus: [index: number]
26-
}>()
2722
</script>
2823

2924
<template>
3025
<article
31-
class="group card-interactive scroll-mt-48 scroll-mb-6 relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50"
26+
class="group card-interactive scroll-mt-48 scroll-mb-6 relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50 focus-within:bg-bg-muted focus-within:border-border-hover"
3227
:class="{
33-
'bg-bg-muted border-border-hover': selected,
3428
'border-accent/30 bg-accent/5': isExactMatch,
3529
}"
3630
>
@@ -50,8 +44,6 @@ const emit = defineEmits<{
5044
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
5145
class="decoration-none scroll-mt-48 scroll-mb-6 after:content-[''] after:absolute after:inset-0"
5246
:data-result-index="index"
53-
@focus="index != null && emit('focus', index)"
54-
@mouseenter="index != null && emit('focus', index)"
5547
>{{ result.package.name }}</NuxtLink
5648
>
5749
<span

app/components/PackageList.vue

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ const props = defineProps<{
2929
pageSize?: PageSize
3030
/** Initial page to scroll to (1-indexed) */
3131
initialPage?: number
32-
/** Selected result index (for keyboard navigation) */
33-
selectedIndex?: number
3432
/** Search query for highlighting exact matches */
3533
searchQuery?: string
3634
/** View mode: cards or table */
@@ -48,8 +46,6 @@ const emit = defineEmits<{
4846
'loadMore': []
4947
/** Emitted when the visible page changes */
5048
'pageChange': [page: number]
51-
/** Emitted when a result is hovered/focused */
52-
'select': [index: number]
5349
/** Emitted when sort option changes (table view) */
5450
'update:sortOption': [option: SortOption]
5551
/** Emitted when a keyword is clicked */
@@ -153,9 +149,7 @@ defineExpose({
153149
:results="displayedResults"
154150
:columns="columns"
155151
v-model:sort-option="sortOption"
156-
:selected-index="selectedIndex"
157152
:is-loading="isLoading"
158-
@select="emit('select', $event)"
159153
@click-keyword="emit('clickKeyword', $event)"
160154
/>
161155
</template>
@@ -179,12 +173,10 @@ defineExpose({
179173
:result="item as NpmSearchResult"
180174
:heading-level="headingLevel"
181175
:show-publisher="showPublisher"
182-
:selected="index === (selectedIndex ?? -1)"
183176
:index="index"
184177
:search-query="searchQuery"
185178
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
186179
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
187-
@focus="emit('select', $event)"
188180
/>
189181
</div>
190182
</template>
@@ -199,7 +191,6 @@ defineExpose({
199191
:result="item"
200192
:heading-level="headingLevel"
201193
:show-publisher="showPublisher"
202-
:selected="index === (selectedIndex ?? -1)"
203194
:index="index"
204195
:search-query="searchQuery"
205196
/>
@@ -230,12 +221,10 @@ defineExpose({
230221
:result="item"
231222
:heading-level="headingLevel"
232223
:show-publisher="showPublisher"
233-
:selected="index === (selectedIndex ?? -1)"
234224
:index="index"
235225
:search-query="searchQuery"
236226
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
237227
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
238-
@focus="emit('select', $event)"
239228
/>
240229
</li>
241230
</ol>

app/components/PackagePlaygrounds.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { PlaygroundLink } from '#shared/types'
3+
import { decodeHtmlEntities } from '~/utils/formatters'
34
45
const props = defineProps<{
56
links: PlaygroundLink[]
@@ -126,7 +127,7 @@ function focusMenuItem(index: number) {
126127
:class="[getIcon(firstLink.provider), getColor(firstLink.provider), 'w-4 h-4 shrink-0']"
127128
aria-hidden="true"
128129
/>
129-
<span class="truncate text-fg-muted">{{ firstLink.label }}</span>
130+
<span class="truncate text-fg-muted">{{ decodeHtmlEntities(firstLink.label) }}</span>
130131
</a>
131132
</AppTooltip>
132133

@@ -182,7 +183,7 @@ function focusMenuItem(index: number) {
182183
:class="[getIcon(link.provider), getColor(link.provider), 'w-4 h-4 shrink-0']"
183184
aria-hidden="true"
184185
/>
185-
<span class="truncate">{{ link.label }}</span>
186+
<span class="truncate">{{ decodeHtmlEntities(link.label) }}</span>
186187
</a>
187188
</AppTooltip>
188189
</div>

0 commit comments

Comments
 (0)