Skip to content

Commit 0990f81

Browse files
essenmitsosseOrbisKcoderabbitai[bot]autofix-ci[bot]knowler
authored
feat: extract button and link component, unify and improve design (#1071)
Co-authored-by: Robin <robin.kehl@singular-it.de> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Nathan Knowler <nathan@knowler.dev> Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent d731543 commit 0990f81

36 files changed

+668
-803
lines changed

app/app.vue

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ if (import.meta.client) {
121121
<template>
122122
<div class="min-h-screen flex flex-col bg-bg text-fg">
123123
<NuxtPwaAssets />
124-
<a href="#main-content" class="skip-link font-mono">{{ $t('common.skip_link') }}</a>
124+
<LinkBase to="#main-content" variant="button-primary" class="skip-link">{{
125+
$t('common.skip_link')
126+
}}</LinkBase>
125127

126128
<AppHeader :show-logo="!isHomepage" />
127129

@@ -140,19 +142,9 @@ if (import.meta.client) {
140142
.skip-link {
141143
position: fixed;
142144
top: -100%;
143-
inset-inline-start: 0;
144-
padding: 0.5rem 1rem;
145-
background: var(--fg);
146-
color: var(--bg);
147-
font-size: 0.875rem;
148145
z-index: 100;
149-
transition: top 0.2s ease;
150146
}
151147
152-
.skip-link:hover {
153-
color: var(--bg);
154-
text-decoration: underline;
155-
}
156148
.skip-link:focus {
157149
top: 0;
158150
}

app/assets/main.css

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ html {
159159
-webkit-font-smoothing: antialiased;
160160
-moz-osx-font-smoothing: grayscale;
161161
text-rendering: optimizeLegibility;
162-
scroll-padding-top: 5rem; /* Offset for fixed header - otherwise anchor headers are cutted */
162+
/* Offset for fixed header - otherwise anchor headers are cutted */
163+
scroll-padding-top: 5rem;
163164
scrollbar-gutter: stable;
164165
}
165166

@@ -185,26 +186,13 @@ body {
185186
line-height: 1.6;
186187
}
187188

188-
/* Default link styling for accessibility on dark background */
189-
a {
190-
color: var(--fg);
191-
text-decoration: underline;
192-
text-underline-offset: 3px;
193-
text-decoration-color: var(--fg-subtle);
194-
transition:
195-
color 0.2s ease,
196-
text-decoration-color 0.2s ease;
197-
}
198-
199-
a:hover {
200-
color: var(--accent);
201-
text-decoration-color: var(--accent);
202-
}
203-
204-
a:focus-visible {
205-
outline: 2px solid var(--accent);
206-
outline-offset: 2px;
207-
border-radius: 4px;
189+
:focus-visible,
190+
:-moz-focusring {
191+
/* weird Firefox behavior makes it necessary to add `!important`
192+
or otherwise the selector would need to be more specific,
193+
which it explicitly should not be. */
194+
outline: 2px solid var(--accent) !important;
195+
outline-offset: 2px !important;
208196
}
209197

210198
/* Reset dd margin (browser default is margin-left: 40px) */
@@ -214,18 +202,7 @@ dd {
214202

215203
/* Reset button styles */
216204
button {
217-
background: transparent;
218-
border: none;
219205
cursor: pointer;
220-
font: inherit;
221-
padding: 0;
222-
}
223-
224-
button:focus-visible,
225-
select:focus-visible {
226-
outline: 2px solid var(--accent);
227-
outline-offset: 2px;
228-
border-radius: 4px;
229206
}
230207

231208
/* Selection */

app/components/AppFooter.vue

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,25 @@ const isHome = computed(() => route.name === 'index')
1414
<BuildEnvironment v-if="!isHome" footer />
1515
</div>
1616
<!-- Desktop: Show all links. Mobile: Links are in MobileMenu -->
17-
<div class="hidden sm:flex items-center gap-6 min-h-11">
18-
<NuxtLink :to="{ name: 'about' }" class="link-subtle font-mono text-xs flex items-center">
17+
<div class="hidden sm:flex items-center gap-6 min-h-11 text-xs">
18+
<LinkBase :to="{ name: 'about' }">
1919
{{ $t('footer.about') }}
20-
</NuxtLink>
21-
<NuxtLink
22-
:to="{ name: 'privacy' }"
23-
class="link-subtle font-mono text-xs flex items-center gap-1"
24-
>
20+
</LinkBase>
21+
<LinkBase :to="{ name: 'privacy' }">
2522
{{ $t('privacy_policy.title') }}
26-
</NuxtLink>
27-
<a
28-
href="https://docs.npmx.dev"
29-
target="_blank"
30-
rel="noopener noreferrer"
31-
class="link-subtle font-mono text-xs flex items-center gap-1"
32-
>
23+
</LinkBase>
24+
<LinkBase to="https://docs.npmx.dev">
3325
{{ $t('footer.docs') }}
34-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
35-
</a>
36-
<a
37-
href="https://repo.npmx.dev"
38-
target="_blank"
39-
rel="noopener noreferrer"
40-
class="link-subtle font-mono text-xs flex items-center gap-1"
41-
>
26+
</LinkBase>
27+
<LinkBase to="https://repo.npmx.dev">
4228
{{ $t('footer.source') }}
43-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
44-
</a>
45-
<a
46-
href="https://social.npmx.dev"
47-
target="_blank"
48-
rel="noopener noreferrer"
49-
class="link-subtle font-mono text-xs flex items-center gap-1"
50-
>
29+
</LinkBase>
30+
<LinkBase to="https://social.npmx.dev">
5131
{{ $t('footer.social') }}
52-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
53-
</a>
54-
<a
55-
href="https://chat.npmx.dev"
56-
target="_blank"
57-
rel="noopener noreferrer"
58-
class="link-subtle font-mono text-xs flex items-center gap-1"
59-
>
32+
</LinkBase>
33+
<LinkBase to="https://chat.npmx.dev">
6034
{{ $t('footer.chat') }}
61-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
62-
</a>
35+
</LinkBase>
6336
</div>
6437
</div>
6538
<p class="text-xs text-fg-muted text-center sm:text-start m-0">

app/components/AppHeader.vue

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import { LinkBase } from '#components'
23
import { isEditableElement } from '~/utils/input'
34
45
withDefaults(
@@ -150,52 +151,39 @@ onKeyStroke(
150151
</div>
151152

152153
<!-- End: Desktop nav items + Mobile menu button -->
153-
<div class="flex-shrink-0 flex items-center gap-0.5 sm:gap-2">
154+
<div class="hidden sm:flex flex-shrink-0">
154155
<!-- Desktop: Compare link -->
155-
<NuxtLink
156+
<LinkBase
157+
class="border-none"
158+
variant="button-secondary"
156159
:to="{ name: 'compare' }"
157-
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"
158-
aria-keyshortcuts="c"
160+
keyshortcut="c"
159161
>
160162
{{ $t('nav.compare') }}
161-
<kbd
162-
class="inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
163-
aria-hidden="true"
164-
>
165-
c
166-
</kbd>
167-
</NuxtLink>
163+
</LinkBase>
168164

169165
<!-- Desktop: Settings link -->
170-
<NuxtLink
166+
<LinkBase
167+
class="border-none"
168+
variant="button-secondary"
171169
:to="{ name: 'settings' }"
172-
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"
173-
aria-keyshortcuts=","
170+
keyshortcut=","
174171
>
175172
{{ $t('nav.settings') }}
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-
,
181-
</kbd>
182-
</NuxtLink>
173+
</LinkBase>
183174

184-
<!-- Desktop: Account menu -->
185-
<div class="hidden sm:block">
186-
<HeaderAccountMenu />
187-
</div>
188-
189-
<!-- Mobile: Menu button (always visible, click to open menu) -->
190-
<button
191-
type="button"
192-
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"
193-
:aria-label="$t('nav.open_menu')"
194-
@click="showMobileMenu = true"
195-
>
196-
<span class="w-6 h-6 inline-block i-carbon:menu" aria-hidden="true" />
197-
</button>
175+
<HeaderAccountMenu />
198176
</div>
177+
178+
<!-- Mobile: Menu button (always visible, click to open menu) -->
179+
<ButtonBase
180+
type="button"
181+
class="sm:hidden flex"
182+
:aria-label="$t('nav.open_menu')"
183+
:aria-expanded="showMobileMenu"
184+
@click="showMobileMenu = !showMobileMenu"
185+
classicon="i-carbon:menu"
186+
/>
199187
</nav>
200188

201189
<!-- Mobile menu -->

app/components/BuildEnvironment.vue

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,19 @@ const buildInfo = useAppConfig().buildInfo
1717
<NuxtTime :datetime="buildInfo.time" :locale="locale" relative />
1818
</i18n-t>
1919
<span>&middot;</span>
20-
<NuxtLink
20+
<LinkBase
2121
v-if="buildInfo.env === 'release'"
22-
external
23-
:href="`https://github.com/npmx-dev/npmx.dev/tag/v${buildInfo.version}`"
24-
target="_blank"
25-
class="hover:text-fg transition-colors"
22+
:to="`https://github.com/npmx-dev/npmx.dev/tag/v${buildInfo.version}`"
2623
>
2724
v{{ buildInfo.version }}
28-
</NuxtLink>
25+
</LinkBase>
2926
<span v-else class="tracking-wider">{{ buildInfo.env }}</span>
3027

3128
<template v-if="buildInfo.commit && buildInfo.branch !== 'release'">
3229
<span>&middot;</span>
33-
<NuxtLink
34-
external
35-
:href="`https://github.com/npmx-dev/npmx.dev/commit/${buildInfo.commit}`"
36-
target="_blank"
37-
class="hover:text-fg transition-colors"
38-
>
30+
<LinkBase :to="`https://github.com/npmx-dev/npmx.dev/commit/${buildInfo.commit}`">
3931
{{ buildInfo.shortCommit }}
40-
</NuxtLink>
32+
</LinkBase>
4133
</template>
4234
</div>
4335
</template>

app/components/Button/Base.vue

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script setup lang="ts">
2+
const props = withDefaults(
3+
defineProps<{
4+
'disabled'?: boolean
5+
'type'?: 'button' | 'submit'
6+
'variant'?: 'primary' | 'secondary'
7+
'size'?: 'small' | 'medium'
8+
'keyshortcut'?: string
9+
10+
/**
11+
* Do not use this directly. Use keyshortcut instead; it generates the correct HTML and displays the shortcut in the UI.
12+
*/
13+
'aria-keyshortcuts'?: never
14+
15+
'classicon'?: string
16+
}>(),
17+
{
18+
type: 'button',
19+
variant: 'secondary',
20+
size: 'medium',
21+
},
22+
)
23+
24+
const el = useTemplateRef('el')
25+
26+
defineExpose({
27+
focus: () => el.value?.focus(),
28+
})
29+
</script>
30+
31+
<template>
32+
<button
33+
ref="el"
34+
class="group cursor-pointer inline-flex gap-x-1 items-center justify-center font-mono border border-border rounded-md transition-all duration-200 disabled:(opacity-40 cursor-not-allowed border-transparent)"
35+
:class="{
36+
'text-sm px-4 py-2': size === 'medium',
37+
'text-xs px-2 py-0.5': size === 'small',
38+
'bg-transparent text-fg hover:enabled:(bg-fg/10) focus-visible:enabled:(bg-fg/10) aria-pressed:(bg-fg text-bg border-fg hover:enabled:(bg-fg text-bg/50))':
39+
variant === 'secondary',
40+
'text-bg bg-fg hover:enabled:(bg-fg/50) focus-visible:enabled:(bg-fg/50) aria-pressed:(bg-fg text-bg border-fg hover:enabled:(text-bg/50))':
41+
variant === 'primary',
42+
}"
43+
:type="props.type"
44+
:disabled="
45+
/**
46+
* Unfortunately Vue _sometimes_ doesn't handle `disabled` correct,
47+
* resulting in an invalid `disabled=false` attribute in the final HTML.
48+
*
49+
* This fixes this.
50+
*/
51+
disabled ? true : undefined
52+
"
53+
:aria-keyshortcuts="keyshortcut"
54+
>
55+
<span
56+
v-if="classicon"
57+
:class="[size === 'small' ? 'size-3' : 'size-4', classicon]"
58+
aria-hidden="true"
59+
/>
60+
<slot />
61+
<kbd
62+
v-if="keyshortcut"
63+
class="ms-2 inline-flex items-center justify-center w-4 h-4 text-xs text-fg bg-bg-muted border border-border rounded no-underline"
64+
aria-hidden="true"
65+
>
66+
{{ keyshortcut }}
67+
</kbd>
68+
</button>
69+
</template>

app/components/Button/Group.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
as?: string | Component
4+
}>()
5+
</script>
6+
7+
<template>
8+
<component
9+
:is="props.as || 'div'"
10+
class="flex items-center shrink-0 ms-auto [&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none"
11+
>
12+
<slot />
13+
</component>
14+
</template>

app/components/ColumnPicker.vue

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,16 @@ function handleReset() {
6969

7070
<template>
7171
<div class="relative">
72-
<button
72+
<ButtonBase
7373
ref="buttonRef"
74-
type="button"
75-
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"
7674
:aria-expanded="isOpen"
7775
aria-haspopup="true"
7876
:aria-controls="menuId"
7977
@click.stop="isOpen = !isOpen"
78+
classicon="i-carbon-column"
8079
>
81-
<span class="i-carbon-column w-4 h-4" aria-hidden="true" />
82-
<span class="font-mono text-sm">{{ $t('filters.columns.title') }}</span>
83-
</button>
80+
{{ $t('filters.columns.title') }}
81+
</ButtonBase>
8482

8583
<Transition name="dropdown">
8684
<div
@@ -136,13 +134,9 @@ function handleReset() {
136134
</div>
137135

138136
<div class="border-t border-border py-1">
139-
<button
140-
type="button"
141-
class="w-full px-3 py-2 text-start 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"
142-
@click="handleReset"
143-
>
137+
<ButtonBase @click="handleReset">
144138
{{ $t('filters.columns.reset') }}
145-
</button>
139+
</ButtonBase>
146140
</div>
147141
</div>
148142
</div>

0 commit comments

Comments
 (0)