Skip to content

Commit daae519

Browse files
committed
fix: add focus-visible states, use Intl date formatting, fix a11y issues
1 parent 8db674b commit daae519

9 files changed

Lines changed: 59 additions & 40 deletions

app/components/ColumnPicker.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function handleReset() {
7878
<button
7979
ref="buttonRef"
8080
type="button"
81-
class="btn-ghost inline-flex items-center gap-1.5 px-3 py-1.5 border border-border rounded-md hover:border-border-hover"
81+
class="btn-ghost inline-flex items-center gap-1.5 px-3 py-1.5 border border-border rounded-md hover:border-border-hover focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
8282
:aria-expanded="isOpen"
8383
aria-haspopup="true"
8484
:aria-controls="menuId"
@@ -140,7 +140,7 @@ function handleReset() {
140140
<div class="border-t border-border py-1">
141141
<button
142142
type="button"
143-
class="w-full px-3 py-2 text-left text-sm font-mono text-fg-muted hover:bg-bg-muted hover:text-fg transition-colors duration-200"
143+
class="w-full px-3 py-2 text-left text-sm font-mono text-fg-muted hover:bg-bg-muted hover:text-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset"
144144
@click="handleReset"
145145
>
146146
{{ $t('filters.columns.reset') }}

app/components/FilterChips.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const emit = defineEmits<{
2121
}}</span>
2222
<button
2323
type="button"
24-
class="ml-0.5 hover:text-fg rounded-full p-0.5 transition-colors duration-200"
24+
class="ml-0.5 hover:text-fg rounded-full p-0.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
2525
:aria-label="$t('filters.remove_filter', { label: chip.label })"
2626
@click="emit('remove', chip)"
2727
>
@@ -33,7 +33,7 @@ const emit = defineEmits<{
3333
<button
3434
v-if="chips.length > 1"
3535
type="button"
36-
class="text-sm text-fg-subtle hover:text-fg underline transition-colors duration-200"
36+
class="text-sm text-fg-subtle hover:text-fg underline transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-2"
3737
@click="emit('clearAll')"
3838
>
3939
{{ $t('filters.clear_all') }}
@@ -44,7 +44,9 @@ const emit = defineEmits<{
4444
<style scoped>
4545
.chip-enter-active,
4646
.chip-leave-active {
47-
transition: all 0.2s ease;
47+
transition:
48+
opacity 0.2s ease,
49+
transform 0.2s ease;
4850
}
4951
5052
.chip-enter-from,

app/components/FilterPanel.vue

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
171171
<!-- Collapsed header -->
172172
<button
173173
type="button"
174-
class="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-bg-muted transition-colors duration-200"
174+
class="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-bg-muted transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset"
175175
:aria-expanded="isExpanded"
176176
@click="isExpanded = !isExpanded"
177177
>
@@ -208,7 +208,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
208208
v-for="option in SEARCH_SCOPE_OPTIONS"
209209
:key="option.value"
210210
type="button"
211-
class="px-2 py-0.5 text-xs font-mono rounded-sm transition-colors duration-200"
211+
class="px-2 py-0.5 text-xs font-mono rounded-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
212212
:class="
213213
filters.searchScope === option.value
214214
? 'bg-bg-muted text-fg'
@@ -227,6 +227,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
227227
type="text"
228228
:value="filters.text"
229229
:placeholder="searchPlaceholder"
230+
autocomplete="off"
230231
class="input-base"
231232
@input="handleTextInput"
232233
/>
@@ -248,7 +249,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
248249
type="button"
249250
role="radio"
250251
:aria-checked="filters.downloadRange === range.value"
251-
class="tag transition-colors duration-200"
252+
class="tag transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
252253
:class="filters.downloadRange === range.value ? 'bg-fg text-bg border-fg' : ''"
253254
@click="emit('update:downloadRange', range.value)"
254255
>
@@ -273,7 +274,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
273274
type="button"
274275
role="radio"
275276
:aria-checked="filters.updatedWithin === option.value"
276-
class="tag transition-colors duration-200"
277+
class="tag transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
277278
:class="filters.updatedWithin === option.value ? 'bg-fg text-bg border-fg' : ''"
278279
@click="emit('update:updatedWithin', option.value)"
279280
>
@@ -298,7 +299,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
298299
role="radio"
299300
disabled
300301
:aria-checked="filters.security === option.value"
301-
class="tag transition-colors duration-200 opacity-50 cursor-not-allowed"
302+
class="tag transition-colors duration-200 opacity-50 cursor-not-allowed focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
302303
:class="filters.security === option.value ? 'bg-fg text-bg border-fg' : ''"
303304
>
304305
{{ $t(getSecurityLabelKey(option.value)) }}
@@ -317,7 +318,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
317318
:key="keyword"
318319
type="button"
319320
:aria-pressed="filters.keywords.includes(keyword)"
320-
class="tag text-xs transition-colors duration-200"
321+
class="tag text-xs transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
321322
:class="filters.keywords.includes(keyword) ? 'bg-fg text-bg border-fg' : ''"
322323
@click="emit('toggleKeyword', keyword)"
323324
>
@@ -326,7 +327,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
326327
<button
327328
v-if="hasMoreKeywords"
328329
type="button"
329-
class="text-xs text-fg-subtle self-center font-mono hover:text-fg transition-colors duration-200"
330+
class="text-xs text-fg-subtle self-center font-mono hover:text-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
330331
@click="showAllKeywords = true"
331332
>
332333
{{ $t('filters.more_keywords', { count: (availableKeywords?.length ?? 0) - 20 }) }}
@@ -341,7 +342,10 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
341342
<style scoped>
342343
.expand-enter-active,
343344
.expand-leave-active {
344-
transition: all 0.2s ease;
345+
transition:
346+
opacity 0.2s ease,
347+
max-height 0.2s ease,
348+
padding 0.2s ease;
345349
overflow: hidden;
346350
}
347351

app/components/PackageList.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ defineExpose({
162162
:selected="index === (selectedIndex ?? -1)"
163163
:index="index"
164164
:search-query="searchQuery"
165-
class="animate-fade-in animate-fill-both"
165+
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
166166
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
167167
@focus="emit('select', $event)"
168168
/>

app/components/PackageListToolbar.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function getSortKeyLabelKey(key: SortKey): string {
134134
<select
135135
id="sort-select"
136136
:value="currentSort.key"
137-
class="appearance-none bg-bg-subtle border border-border rounded-md pl-3 pr-8 py-1.5 font-mono text-sm text-fg cursor-pointer transition-colors duration-200 focus:(border-border-hover outline-none) hover:border-border-hover"
137+
class="appearance-none bg-bg-subtle border border-border rounded-md pl-3 pr-8 py-1.5 font-mono text-sm text-fg cursor-pointer transition-colors duration-200 hover:border-border-hover focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
138138
@change="handleSortKeyChange"
139139
>
140140
<option
@@ -158,7 +158,7 @@ function getSortKeyLabelKey(key: SortKey): string {
158158
<!-- Sort direction toggle -->
159159
<button
160160
type="button"
161-
class="p-1.5 rounded border border-border bg-bg-subtle text-fg-muted hover:text-fg hover:border-border-hover transition-colors duration-200"
161+
class="p-1.5 rounded border border-border bg-bg-subtle text-fg-muted hover:text-fg hover:border-border-hover transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
162162
:aria-label="$t('filters.sort.toggle_direction')"
163163
:title="
164164
currentSort.direction === 'asc'

app/components/PackageTable.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ function getColumnLabelKey(id: ColumnId): string {
122122
: undefined
123123
"
124124
:tabindex="isSortable('name') ? 0 : undefined"
125-
:role="isSortable('name') ? 'columnheader button' : 'columnheader'"
125+
role="columnheader"
126+
class="focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset focus-visible:outline-none"
126127
@click="toggleSort('name')"
127128
@keydown.enter="toggleSort('name')"
128129
@keydown.space.prevent="toggleSort('name')"
@@ -173,7 +174,8 @@ function getColumnLabelKey(id: ColumnId): string {
173174
: undefined
174175
"
175176
:tabindex="isSortable('downloads') ? 0 : undefined"
176-
:role="isSortable('downloads') ? 'columnheader button' : 'columnheader'"
177+
role="columnheader"
178+
class="focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset focus-visible:outline-none"
177179
@click="toggleSort('downloads')"
178180
@keydown.enter="toggleSort('downloads')"
179181
@keydown.space.prevent="toggleSort('downloads')"
@@ -207,7 +209,8 @@ function getColumnLabelKey(id: ColumnId): string {
207209
: undefined
208210
"
209211
:tabindex="isSortable('updated') ? 0 : undefined"
210-
:role="isSortable('updated') ? 'columnheader button' : 'columnheader'"
212+
role="columnheader"
213+
class="focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset focus-visible:outline-none"
211214
@click="toggleSort('updated')"
212215
@keydown.enter="toggleSort('updated')"
213216
@keydown.space.prevent="toggleSort('updated')"

app/components/PackageTableRow.vue

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,22 @@ function formatDate(dateStr?: string): string {
2929
const date = new Date(dateStr)
3030
const now = new Date()
3131
const diffMs = now.getTime() - date.getTime()
32-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
33-
34-
if (diffDays === 0) return 'Today'
35-
if (diffDays === 1) return 'Yesterday'
36-
if (diffDays < 7) return `${diffDays}d ago`
37-
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`
38-
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`
39-
return `${Math.floor(diffDays / 365)}y ago`
32+
const diffSeconds = Math.floor(diffMs / 1000)
33+
const diffMinutes = Math.floor(diffSeconds / 60)
34+
const diffHours = Math.floor(diffMinutes / 60)
35+
const diffDays = Math.floor(diffHours / 24)
36+
const diffWeeks = Math.floor(diffDays / 7)
37+
const diffMonths = Math.floor(diffDays / 30)
38+
const diffYears = Math.floor(diffDays / 365)
39+
40+
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
41+
42+
if (diffDays === 0) return rtf.format(0, 'day')
43+
if (diffDays === 1) return rtf.format(-1, 'day')
44+
if (diffDays < 7) return rtf.format(-diffDays, 'day')
45+
if (diffDays < 30) return rtf.format(-diffWeeks, 'week')
46+
if (diffDays < 365) return rtf.format(-diffMonths, 'month')
47+
return rtf.format(-diffYears, 'year')
4048
}
4149
4250
function formatScore(value?: number): string {
@@ -58,7 +66,7 @@ const allMaintainersText = computed(() => {
5866

5967
<template>
6068
<tr
61-
class="border-b border-border hover:bg-bg-muted transition-colors duration-200"
69+
class="border-b border-border hover:bg-bg-muted transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset focus-visible:outline-none"
6270
:class="{ 'bg-bg-muted': selected }"
6371
tabindex="0"
6472
@focus="emit('focus')"
@@ -131,7 +139,7 @@ const allMaintainersText = computed(() => {
131139
v-for="keyword in pkg.keywords.slice(0, 3)"
132140
:key="keyword"
133141
type="button"
134-
class="tag text-xs hover:bg-fg hover:text-bg hover:border-fg transition-colors duration-200"
142+
class="tag text-xs hover:bg-fg hover:text-bg hover:border-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
135143
:title="`Filter by ${keyword}`"
136144
@click.stop="emit('clickKeyword', keyword)"
137145
>
@@ -179,10 +187,12 @@ const allMaintainersText = computed(() => {
179187
<!-- Security -->
180188
<td v-if="isColumnVisible('security')" class="py-2 px-3">
181189
<span v-if="result.flags?.insecure" class="text-syntax-kw">
182-
<span class="i-carbon-warning w-4 h-4" :aria-label="$t('filters.table.security_warning')" />
190+
<span class="i-carbon-warning w-4 h-4" aria-hidden="true" />
191+
<span class="sr-only">{{ $t('filters.table.security_warning') }}</span>
183192
</span>
184193
<span v-else-if="result.flags !== undefined" class="text-provider-nuxt">
185-
<span class="i-carbon-checkmark w-4 h-4" :aria-label="$t('filters.table.secure')" />
194+
<span class="i-carbon-checkmark w-4 h-4" aria-hidden="true" />
195+
<span class="sr-only">{{ $t('filters.table.secure') }}</span>
186196
</span>
187197
<span v-else class="text-fg-subtle"> - </span>
188198
</td>

app/components/PaginationControls.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function handlePageSizeChange(event: Event) {
105105
>
106106
<button
107107
type="button"
108-
class="px-2.5 py-1 text-xs font-mono rounded-sm transition-colors duration-200"
108+
class="px-2.5 py-1 text-xs font-mono rounded-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
109109
:class="mode === 'infinite' ? 'bg-bg-muted text-fg' : 'text-fg-muted hover:text-fg'"
110110
:aria-pressed="mode === 'infinite'"
111111
@click="emit('update:mode', 'infinite')"
@@ -114,7 +114,7 @@ function handlePageSizeChange(event: Event) {
114114
</button>
115115
<button
116116
type="button"
117-
class="px-2.5 py-1 text-xs font-mono rounded-sm transition-colors duration-200"
117+
class="px-2.5 py-1 text-xs font-mono rounded-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
118118
:class="mode === 'paginated' ? 'bg-bg-muted text-fg' : 'text-fg-muted hover:text-fg'"
119119
:aria-pressed="mode === 'paginated'"
120120
@click="emit('update:mode', 'paginated')"
@@ -129,7 +129,7 @@ function handlePageSizeChange(event: Event) {
129129
<select
130130
id="page-size"
131131
:value="pageSize"
132-
class="appearance-none bg-bg-subtle border border-border rounded-md pl-3 pr-8 py-1 font-mono text-sm text-fg cursor-pointer transition-colors duration-200 focus:(border-border-hover outline-none) hover:border-border-hover"
132+
class="appearance-none bg-bg-subtle border border-border rounded-md pl-3 pr-8 py-1 font-mono text-sm text-fg cursor-pointer transition-colors duration-200 hover:border-border-hover focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
133133
@change="handlePageSizeChange"
134134
>
135135
<option v-for="size in PAGE_SIZE_OPTIONS" :key="size" :value="size">
@@ -167,7 +167,7 @@ function handlePageSizeChange(event: Event) {
167167
<!-- Previous button -->
168168
<button
169169
type="button"
170-
class="p-1.5 rounded hover:bg-bg-muted text-fg-muted hover:text-fg disabled:opacity-40 disabled:cursor-not-allowed transition-colors duration-200"
170+
class="p-1.5 rounded hover:bg-bg-muted text-fg-muted hover:text-fg disabled:opacity-40 disabled:cursor-not-allowed transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
171171
:disabled="!canGoPrev"
172172
:aria-label="$t('filters.pagination.previous')"
173173
@click="goPrev"
@@ -177,11 +177,11 @@ function handlePageSizeChange(event: Event) {
177177

178178
<!-- Page numbers -->
179179
<template v-for="(page, idx) in visiblePages" :key="idx">
180-
<span v-if="page === 'ellipsis'" class="px-2 text-fg-subtle font-mono"> ... </span>
180+
<span v-if="page === 'ellipsis'" class="px-2 text-fg-subtle font-mono"></span>
181181
<button
182182
v-else
183183
type="button"
184-
class="min-w-[32px] h-8 px-2 font-mono text-sm rounded transition-colors duration-200"
184+
class="min-w-[32px] h-8 px-2 font-mono text-sm rounded transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
185185
:class="
186186
page === currentPage
187187
? 'bg-fg text-bg'
@@ -197,7 +197,7 @@ function handlePageSizeChange(event: Event) {
197197
<!-- Next button -->
198198
<button
199199
type="button"
200-
class="p-1.5 rounded hover:bg-bg-muted text-fg-muted hover:text-fg disabled:opacity-40 disabled:cursor-not-allowed transition-colors duration-200"
200+
class="p-1.5 rounded hover:bg-bg-muted text-fg-muted hover:text-fg disabled:opacity-40 disabled:cursor-not-allowed transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
201201
:disabled="!canGoNext"
202202
:aria-label="$t('filters.pagination.next')"
203203
@click="goNext"

app/components/ViewModeToggle.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const viewMode = defineModel<ViewMode>({ default: 'cards' })
1212
>
1313
<button
1414
type="button"
15-
class="inline-flex items-center px-2.5 py-1.5 text-sm font-medium rounded-sm transition-colors duration-200"
15+
class="inline-flex items-center px-2.5 py-1.5 text-sm font-medium rounded-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
1616
:class="viewMode === 'cards' ? 'bg-bg-muted text-fg' : 'text-fg-muted hover:text-fg'"
1717
:aria-pressed="viewMode === 'cards'"
1818
:aria-label="$t('filters.view_mode.cards')"
@@ -23,7 +23,7 @@ const viewMode = defineModel<ViewMode>({ default: 'cards' })
2323
</button>
2424
<button
2525
type="button"
26-
class="inline-flex items-center px-2.5 py-1.5 text-sm font-medium rounded-sm transition-colors duration-200"
26+
class="inline-flex items-center px-2.5 py-1.5 text-sm font-medium rounded-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
2727
:class="viewMode === 'table' ? 'bg-bg-muted text-fg' : 'text-fg-muted hover:text-fg'"
2828
:aria-pressed="viewMode === 'table'"
2929
:aria-label="$t('filters.view_mode.table')"

0 commit comments

Comments
 (0)