Skip to content

Commit 233f6a5

Browse files
essenmitsosseOrbisKcoderabbitai[bot]autofix-ci[bot]knowler
authored andcommitted
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 0ec67fb commit 233f6a5

36 files changed

Lines changed: 670 additions & 808 deletions

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
@@ -160,7 +160,8 @@ html {
160160
-webkit-font-smoothing: antialiased;
161161
-moz-osx-font-smoothing: grayscale;
162162
text-rendering: optimizeLegibility;
163-
scroll-padding-top: 5rem; /* Offset for fixed header - otherwise anchor headers are cutted */
163+
/* Offset for fixed header - otherwise anchor headers are cutted */
164+
scroll-padding-top: 5rem;
164165
scrollbar-gutter: stable;
165166
}
166167

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

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

211199
/* Reset dd margin (browser default is margin-left: 40px) */
@@ -215,18 +203,7 @@ dd {
215203

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

232209
/* Selection */

app/components/AppFooter.vue

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,58 +14,28 @@ 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: 'blog' }"
23-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
24-
>
20+
</LinkBase>
21+
<LinkBase :to="{ name: 'blog' }">
2522
{{ $t('footer.blog') }}
26-
</NuxtLink>
27-
<NuxtLink
28-
:to="{ name: 'privacy' }"
29-
class="link-subtle font-mono text-xs flex items-center gap-1"
30-
>
23+
</LinkBase>
24+
<LinkBase :to="{ name: 'privacy' }">
3125
{{ $t('privacy_policy.title') }}
32-
</NuxtLink>
33-
<a
34-
href="https://docs.npmx.dev"
35-
target="_blank"
36-
rel="noopener noreferrer"
37-
class="link-subtle font-mono text-xs flex items-center gap-1"
38-
>
26+
</LinkBase>
27+
<LinkBase to="https://docs.npmx.dev">
3928
{{ $t('footer.docs') }}
40-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
41-
</a>
42-
<a
43-
href="https://repo.npmx.dev"
44-
target="_blank"
45-
rel="noopener noreferrer"
46-
class="link-subtle font-mono text-xs flex items-center gap-1"
47-
>
29+
</LinkBase>
30+
<LinkBase to="https://repo.npmx.dev">
4831
{{ $t('footer.source') }}
49-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
50-
</a>
51-
<a
52-
href="https://social.npmx.dev"
53-
target="_blank"
54-
rel="noopener noreferrer"
55-
class="link-subtle font-mono text-xs flex items-center gap-1"
56-
>
32+
</LinkBase>
33+
<LinkBase to="https://social.npmx.dev">
5734
{{ $t('footer.social') }}
58-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
59-
</a>
60-
<a
61-
href="https://chat.npmx.dev"
62-
target="_blank"
63-
rel="noopener noreferrer"
64-
class="link-subtle font-mono text-xs flex items-center gap-1"
65-
>
35+
</LinkBase>
36+
<LinkBase to="https://chat.npmx.dev">
6637
{{ $t('footer.chat') }}
67-
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
68-
</a>
38+
</LinkBase>
6939
</div>
7040
</div>
7141
<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>

0 commit comments

Comments
 (0)