Skip to content

Commit 6de6988

Browse files
Merge branch 'main' into feat/make-fund-more-visible
2 parents b4129e4 + 8cd4074 commit 6de6988

Some content is hidden

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

69 files changed

+2823
-539
lines changed

.storybook/main.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import type { StorybookConfig } from '@storybook-vue/nuxt'
22

33
const config = {
4-
stories: ['../app/**/*.stories.@(js|ts)'],
5-
addons: ['@storybook/addon-a11y', '@storybook/addon-docs', '@storybook/addon-themes'],
4+
stories: ['../.storybook/*.mdx', '../app/**/*.stories.@(js|ts)'],
5+
addons: [
6+
'@storybook/addon-a11y',
7+
'@storybook/addon-docs',
8+
'@storybook/addon-themes',
9+
'storybook-i18n',
10+
],
611
framework: '@storybook-vue/nuxt',
712
staticDirs: ['./.public'],
813
features: {
@@ -11,6 +16,33 @@ const config = {
1116
async viteFinal(newConfig) {
1217
newConfig.plugins ??= []
1318

19+
// Bridge compatibility between Storybook v10 core and v9 @storybook-vue/nuxt
20+
// v10 expects module federation globals that v9 doesn't provide
21+
newConfig.plugins.push({
22+
name: 'storybook-v10-compat',
23+
transformIndexHtml: {
24+
order: 'pre',
25+
handler(html) {
26+
const script = `
27+
<script>
28+
// Minimal shims for Storybook v10 module federation system
29+
// These will be replaced when Storybook runtime loads
30+
window.__STORYBOOK_MODULE_GLOBAL__ = { global: window };
31+
window.__STORYBOOK_MODULE_CLIENT_LOGGER__ = {
32+
deprecate: console.warn.bind(console, '[deprecated]'),
33+
once: console.log.bind(console),
34+
logger: console
35+
};
36+
window.__STORYBOOK_MODULE_CHANNELS__ = {
37+
Channel: class { on() {} off() {} emit() {} once() {} },
38+
createBrowserChannel: () => new window.__STORYBOOK_MODULE_CHANNELS__.Channel()
39+
};
40+
</script>`
41+
return html.replace(/<script>/, script + '<script>')
42+
},
43+
},
44+
})
45+
1446
newConfig.plugins.push({
1547
name: 'ignore-internals',
1648
transform(_, id) {

.storybook/manager.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { addons } from 'storybook/manager-api'
2-
import { create } from 'storybook/theming'
32

4-
const npmxTheme = create({
5-
brandTitle: 'npmx Storybook',
6-
brandImage: '/npmx-storybook.svg',
7-
})
3+
import npmxDark from './theme'
84

95
addons.setConfig({
10-
theme: npmxTheme,
6+
theme: npmxDark,
7+
layoutCustomisations: {
8+
showToolbar: (state, defaultValue) => {
9+
if (state.viewMode === 'docs' && state.storyId) {
10+
const story = state.index?.[state.storyId]
11+
const tags = story?.tags || []
12+
if (tags.includes('hide-toolbar')) {
13+
return false
14+
}
15+
}
16+
return defaultValue
17+
},
18+
},
1119
})

.storybook/preview-head.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<style>
2+
/* Override docs story canvas background to match npmx theme */
3+
.docs-story {
4+
background-color: var(--bg, oklch(0.171 0 0)) !important;
5+
}
6+
</style>

.storybook/preview.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { Preview } from '@storybook-vue/nuxt'
22
import { withThemeByDataAttribute } from '@storybook/addon-themes'
3+
import { addons } from 'storybook/preview-api'
34
import { currentLocales } from '../config/i18n'
45
import { fn } from 'storybook/test'
56
import { ACCENT_COLORS } from '../shared/utils/constants'
67

8+
import npmxDark from './theme'
9+
710
// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
811
// Stub Nuxt specific globals
912
// @ts-expect-error - dynamic global name
@@ -17,6 +20,12 @@ globalThis['__NUXT_COLOR_MODE__'] ??= {
1720
// @ts-expect-error - dynamic global name
1821
globalThis.defineOgImageComponent = fn()
1922

23+
// Subscribe to locale changes from storybook-i18n addon (once, outside decorator)
24+
let currentI18nInstance: any = null
25+
addons.getChannel().on('LOCALE_CHANGED', (newLocale: string) => {
26+
currentI18nInstance?.setLocale(newLocale)
27+
})
28+
2029
const preview: Preview = {
2130
parameters: {
2231
controls: {
@@ -25,25 +34,22 @@ const preview: Preview = {
2534
date: /Date$/i,
2635
},
2736
},
37+
docs: {
38+
theme: npmxDark,
39+
},
40+
},
41+
initialGlobals: {
42+
locale: 'en-US',
43+
locales: currentLocales.reduce(
44+
(acc, locale) => {
45+
acc[locale.code] = locale.name
46+
return acc
47+
},
48+
{} as Record<string, string>,
49+
),
2850
},
2951
// Provides toolbars to switch things like theming and language
3052
globalTypes: {
31-
locale: {
32-
name: 'Locale',
33-
description: 'UI language',
34-
defaultValue: 'en-US',
35-
toolbar: {
36-
icon: 'globe',
37-
dynamicTitle: true,
38-
items: [
39-
// English is at the top so it's easier to reset to it
40-
{ value: 'en-US', title: 'English (US)' },
41-
...currentLocales
42-
.filter(locale => locale.code !== 'en-US')
43-
.map(locale => ({ value: locale.code, title: locale.name })),
44-
],
45-
},
46-
},
4753
accentColor: {
4854
name: 'Accent Color',
4955
description: 'Accent color',
@@ -70,9 +76,9 @@ const preview: Preview = {
7076
attributeName: 'data-theme',
7177
}),
7278
(story, context) => {
73-
const { locale, accentColor } = context.globals as {
74-
locale: string
79+
const { accentColor, locale } = context.globals as {
7580
accentColor?: string
81+
locale?: string
7682
}
7783

7884
// Set accent color from globals
@@ -84,14 +90,12 @@ const preview: Preview = {
8490

8591
return {
8692
template: '<story />',
87-
// Set locale from globals
8893
created() {
89-
if (this.$i18n) {
90-
this.$i18n.setLocale(locale)
91-
}
92-
},
93-
updated() {
94-
if (this.$i18n) {
94+
// Store i18n instance for LOCALE_CHANGED events
95+
currentI18nInstance = this.$i18n
96+
97+
// Set initial locale when component is created
98+
if (locale && this.$i18n) {
9599
this.$i18n.setLocale(locale)
96100
}
97101
},

.storybook/storybook-welcome.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Meta } from '@storybook/addon-docs/blocks';
2+
3+
<Meta title="Welcome" tags={['hide-toolbar']}/>
4+
5+
# Welcome
6+
7+
Welcome to the npmx Storybook.

.storybook/theme.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { create } from 'storybook/theming'
2+
3+
const npmxDark = create({
4+
base: 'dark',
5+
6+
brandTitle: 'npmx Storybook',
7+
brandImage: '/npmx-storybook.svg',
8+
9+
// UI
10+
appContentBg: '#101010', // oklch(0.171 0 0)
11+
})
12+
13+
export default npmxDark

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/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"

0 commit comments

Comments
 (0)