Skip to content

Commit f48b384

Browse files
Merge branch 'main' into feat/download-button
2 parents 5e0882f + b43cc0a commit f48b384

Some content is hidden

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

72 files changed

+2492
-657
lines changed

app/components/AppFooter.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const closeModal = () => modalRef.value?.close?.()
3535
<LinkBase :to="{ name: 'accessibility' }">
3636
{{ $t('a11y.footer_title') }}
3737
</LinkBase>
38+
<LinkBase :to="{ name: 'translation-status' }">
39+
{{ $t('translation_status.title') }}
40+
</LinkBase>
3841
<button
3942
type="button"
4043
class="cursor-pointer group inline-flex gap-x-1 items-center justify-center underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200"

app/components/AppHeader.vue

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ const mobileLinks = computed<NavigationConfigWithGroups>(() => [
8484
external: false,
8585
iconClass: 'i-custom:a11y',
8686
},
87+
{
88+
name: 'Translation Status',
89+
label: $t('translation_status.title'),
90+
to: { name: 'translation-status' },
91+
type: 'link',
92+
external: false,
93+
iconClass: 'i-lucide:languages',
94+
},
8795
],
8896
},
8997
{
@@ -235,16 +243,18 @@ onKeyStroke(
235243
{{ env === 'release' ? 'alpha' : env }}
236244
</span>
237245
</NuxtLink>
238-
<NuxtLink
239-
v-if="prNumber"
240-
:to="`https://github.com/npmx-dev/npmx.dev/pull/${prNumber}`"
241-
:aria-label="`Open GitHub pull request ${prNumber}`"
242-
>
243-
<span class="text-xs px-1.5 py-0.5 rounded badge-green font-sans font-medium ms-2">
244-
PR #{{ prNumber }}
245-
</span>
246-
</NuxtLink>
247246
</div>
247+
248+
<NuxtLink
249+
v-if="showLogo && !isSearchExpanded && prNumber"
250+
:to="`https://github.com/npmx-dev/npmx.dev/pull/${prNumber}`"
251+
:aria-label="$t('header.pr', { prNumber })"
252+
>
253+
<span class="text-xs px-1.5 py-0.5 rounded badge-green font-sans font-medium">
254+
PR #{{ prNumber }}
255+
</span>
256+
</NuxtLink>
257+
248258
<!-- Spacer when logo is hidden on desktop -->
249259
<span v-else class="hidden sm:block w-1" />
250260

app/components/BackButton.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup lang="ts">
2+
const router = useRouter()
3+
const canGoBack = useCanGoBack()
4+
</script>
5+
6+
<template>
7+
<button
8+
v-if="canGoBack"
9+
type="button"
10+
class="inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
11+
@click="router.back()"
12+
>
13+
<span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
14+
<span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
15+
</button>
16+
</template>

app/components/BlueskyComment.vue

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,27 @@ function getHostname(uri: string): string {
3838
return uri
3939
}
4040
}
41+
42+
let segmenter: Intl.Segmenter | null = null
43+
function firstChar(str: string): string {
44+
segmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' })
45+
return Array.from(segmenter.segment(str))[0]?.segment ?? ''
46+
}
4147
</script>
4248

4349
<template>
44-
<div :class="depth === 0 ? 'flex gap-3' : 'flex gap-3 mt-3'">
45-
<!-- Avatar -->
50+
<!--
51+
Depth 0: classic avatar-column layout (all screens)
52+
Depth 1+: Medium-style inline avatar on mobile, avatar-column on desktop
53+
-->
54+
<div :class="depth === 0 ? 'flex gap-3' : 'sm:flex sm:gap-3'">
55+
<!-- Column avatar: always shown at depth 0, desktop-only at depth 1+ -->
4656
<a
4757
:href="`https://bsky.app/profile/${comment.author.handle}`"
4858
target="_blank"
4959
rel="noopener noreferrer"
5060
class="shrink-0"
61+
:class="depth > 0 ? 'hidden sm:block' : ''"
5162
>
5263
<img
5364
v-if="comment.author.avatar"
@@ -65,22 +76,45 @@ function getHostname(uri: string): string {
6576
depth === 0 ? 'w-10 h-10' : 'w-8 h-8 text-sm',
6677
]"
6778
>
68-
{{ (comment.author.displayName || comment.author.handle).charAt(0).toUpperCase() }}
79+
{{ firstChar(comment.author.displayName || comment.author.handle).toUpperCase() }}
6980
</div>
7081
</a>
7182

7283
<div class="flex-1 min-w-0">
7384
<!-- Author info + timestamp -->
74-
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0">
85+
<div class="flex flex-wrap items-center gap-x-2 gap-y-0">
86+
<!-- Inline avatar: mobile-only for nested comments -->
87+
<a
88+
v-if="depth > 0"
89+
:href="`https://bsky.app/profile/${comment.author.handle}`"
90+
target="_blank"
91+
rel="noopener noreferrer"
92+
class="shrink-0 sm:hidden"
93+
>
94+
<img
95+
v-if="comment.author.avatar"
96+
:src="comment.author.avatar"
97+
:alt="comment.author.displayName || comment.author.handle"
98+
class="w-6 h-6 rounded-full"
99+
width="24"
100+
height="24"
101+
loading="lazy"
102+
/>
103+
<div
104+
v-else
105+
class="w-6 h-6 rounded-full bg-border flex items-center justify-center text-fg-muted text-xs"
106+
>
107+
{{ firstChar(comment.author.displayName || comment.author.handle).toUpperCase() }}
108+
</div>
109+
</a>
75110
<a
76111
:href="`https://bsky.app/profile/${comment.author.handle}`"
77112
target="_blank"
78113
rel="noopener noreferrer"
79-
class="font-medium text-fg hover:underline"
114+
:class="['font-medium text-fg hover:underline', depth > 0 ? 'text-sm' : '']"
80115
>
81116
{{ comment.author.displayName || comment.author.handle }}
82117
</a>
83-
<span class="text-fg-subtle text-sm">@{{ comment.author.handle }}</span>
84118
<span class="text-fg-subtle text-sm">·</span>
85119
<a
86120
:href="getCommentUrl(props.comment)"
@@ -93,7 +127,7 @@ function getHostname(uri: string): string {
93127
</div>
94128

95129
<!-- Comment text with rich segments -->
96-
<p class="text-fg-muted whitespace-pre-wrap">
130+
<p class="text-fg-muted whitespace-pre-wrap mt-2 mb-3">
97131
<template v-for="(segment, i) in processedSegments" :key="i">
98132
<a
99133
v-if="segment.url"
@@ -162,7 +196,7 @@ function getHostname(uri: string): string {
162196
<!-- Like/repost counts -->
163197
<div
164198
v-if="comment.likeCount > 0 || comment.repostCount > 0"
165-
class="mt-2 flex gap-4 text-sm text-fg-subtle"
199+
class="mt-1 flex gap-4 text-sm text-fg-subtle"
166200
>
167201
<span v-if="comment.likeCount > 0">
168202
{{ $t('blog.atproto.like_count', { count: comment.likeCount }, comment.likeCount) }}
@@ -174,7 +208,10 @@ function getHostname(uri: string): string {
174208

175209
<!-- Nested replies -->
176210
<template v-if="comment.replies.length > 0">
177-
<div v-if="depth < MaxDepth" class="mt-2 ps-2 border-is-2 border-border flex flex-col">
211+
<div
212+
v-if="depth < MaxDepth"
213+
class="mt-3 ps-3 border-is-2 border-border flex flex-col gap-3"
214+
>
178215
<BlueskyComment
179216
v-for="reply in comment.replies"
180217
:key="reply.uri"

app/components/Button/Base.stories.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.

app/components/Button/Base.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
<script setup lang="ts">
22
import type { IconClass } from '~/types'
33
4+
/**
5+
* A base button component that supports multiple variants, sizes, and states as well as icons and keyboard shortcuts.
6+
*/
7+
defineOptions({
8+
name: 'ButtonBase',
9+
})
10+
411
const props = withDefaults(
512
defineProps<{
613
disabled?: boolean
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
2+
import ButtonBase from './Base.vue'
3+
4+
const meta = {
5+
component: ButtonBase,
6+
parameters: {
7+
docs: {
8+
source: {
9+
type: 'dynamic',
10+
transform: (code: string) =>
11+
code.replace(/<Base\b/g, '<ButtonBase').replace(/<\/Base>/g, '</ButtonBase>'),
12+
},
13+
},
14+
},
15+
tags: ['autodocs'],
16+
} satisfies Meta<typeof ButtonBase>
17+
18+
export default meta
19+
type Story = StoryObj<typeof meta>
20+
21+
export const Default: Story = {
22+
args: {
23+
default: 'Button Text',
24+
},
25+
}
26+
27+
export const Primary: Story = {
28+
args: {
29+
variant: 'primary',
30+
},
31+
render: args => ({
32+
components: { ButtonBase },
33+
setup() {
34+
return { args }
35+
},
36+
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
37+
}),
38+
}
39+
40+
export const Secondary: Story = {
41+
args: {
42+
variant: 'secondary',
43+
},
44+
render: args => ({
45+
components: { ButtonBase },
46+
setup() {
47+
return { args }
48+
},
49+
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
50+
}),
51+
}
52+
53+
export const Small: Story = {
54+
args: {
55+
size: 'small',
56+
},
57+
render: args => ({
58+
components: { ButtonBase },
59+
setup() {
60+
return { args }
61+
},
62+
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
63+
}),
64+
}
65+
66+
export const Disabled: Story = {
67+
args: {
68+
disabled: true,
69+
},
70+
render: args => ({
71+
components: { ButtonBase },
72+
setup() {
73+
return { args }
74+
},
75+
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
76+
}),
77+
}
78+
79+
export const WithIcon: Story = {
80+
args: {
81+
classicon: 'i-lucide:search',
82+
},
83+
render: args => ({
84+
components: { ButtonBase },
85+
setup() {
86+
return { args }
87+
},
88+
template: `<ButtonBase v-bind="args">{{ $t("search.button") }}</ButtonBase>`,
89+
}),
90+
}
91+
92+
export const WithKeyboardShortcut: Story = {
93+
args: {
94+
ariaKeyshortcuts: '/',
95+
},
96+
render: args => ({
97+
components: { ButtonBase },
98+
setup() {
99+
return { args }
100+
},
101+
template: `<ButtonBase v-bind="args">{{ $t("search.button") }}</ButtonBase>`,
102+
}),
103+
}
104+
105+
export const Block: Story = {
106+
args: {
107+
block: true,
108+
},
109+
render: args => ({
110+
components: { ButtonBase },
111+
setup() {
112+
return { args }
113+
},
114+
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
115+
}),
116+
}

0 commit comments

Comments
 (0)