Skip to content

Commit ad353f3

Browse files
authored
Merge branch 'main' into feat/resolve-40
2 parents 1ddcb21 + 8b5edf2 commit ad353f3

18 files changed

Lines changed: 491 additions & 176 deletions

.storybook/main.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { resolve } from 'node:path'
55
const config = {
66
stories: [
77
// List welcome first in sidebar
8-
'../.storybook/docs/welcome.mdx',
9-
'../.storybook/docs/*.mdx',
8+
'../app/storybook/welcome.mdx',
109
'../app/**/*.@(mdx|stories.@(js|ts))',
1110
],
1211
addons: [

app/components/Brand/Customize.vue

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ async function downloadCustomPng() {
7575
if (!svg) return
7676
pngLoading.value = true
7777
78-
const blob = new Blob([svg], { type: 'image/svg+xml' })
79-
const url = URL.createObjectURL(blob)
78+
const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
8079
8180
try {
8281
await document.fonts.ready
@@ -108,7 +107,6 @@ async function downloadCustomPng() {
108107
}, 'image/png')
109108
})
110109
} finally {
111-
URL.revokeObjectURL(url)
112110
pngLoading.value = false
113111
}
114112
}
@@ -145,24 +143,30 @@ async function downloadCustomPng() {
145143
<span class="text-sm font-mono text-fg-muted shrink-0">{{
146144
$t('brand.customize.accent_label')
147145
}}</span>
148-
<div class="flex items-center gap-1.5" role="radiogroup">
149-
<ButtonBase
146+
<div class="flex items-center gap-1.5">
147+
<label
150148
v-for="color in pickerColors"
151149
:key="color.id"
152-
role="radio"
153-
:aria-checked="activeAccentId === color.id"
154-
:aria-label="color.label"
155-
class="!w-6 !h-6 !rounded-full !border-2 !p-0 !min-w-0 transition-all duration-150 motion-reduce:transition-none"
150+
class="relative w-6 h-6 rounded-full border-2 cursor-pointer duration-150 motion-reduce:transition-none focus-within:ring-2 focus-within:ring-fg focus-within:ring-offset-2 focus-within:ring-offset-bg"
156151
:class="
157152
activeAccentId === color.id
158-
? '!border-fg scale-110'
153+
? 'border-fg scale-110'
159154
: color.id === 'neutral'
160-
? '!border-border hover:!border-border-hover'
161-
: '!border-transparent hover:!border-border-hover'
155+
? 'border-border hover:border-border-hover'
156+
: 'border-transparent hover:border-border-hover'
162157
"
163158
:style="{ backgroundColor: color.value }"
164-
@click="customAccent = color.id"
165-
/>
159+
>
160+
<input
161+
type="radio"
162+
name="brand-customize-accent"
163+
:value="color.id"
164+
:checked="activeAccentId === color.id"
165+
:aria-label="color.label"
166+
class="sr-only"
167+
@change="customAccent = color.id"
168+
/>
169+
</label>
166170
</div>
167171
</fieldset>
168172

@@ -172,40 +176,33 @@ async function downloadCustomPng() {
172176
<span class="text-sm font-mono text-fg-muted">{{
173177
$t('brand.customize.bg_label')
174178
}}</span>
175-
<div
176-
class="flex items-center border border-border rounded-md overflow-hidden"
177-
role="radiogroup"
178-
>
179-
<ButtonBase
180-
size="md"
181-
role="radio"
182-
:aria-checked="customBgDark"
183-
:aria-label="$t('brand.logos.on_dark')"
184-
class="!border-none !rounded-none motion-reduce:transition-none"
185-
:class="
186-
customBgDark
187-
? 'bg-bg-muted text-fg'
188-
: 'bg-transparent text-fg-muted hover:text-fg'
189-
"
190-
@click="customBgDark = true"
179+
<div class="flex items-center border border-border rounded-md overflow-hidden">
180+
<label
181+
class="px-3 py-1.5 text-sm font-mono cursor-pointer motion-reduce:transition-none focus-within:bg-fg/10"
182+
:class="customBgDark ? 'bg-bg-muted text-fg' : 'text-fg-muted hover:text-fg'"
191183
>
184+
<input
185+
v-model="customBgDark"
186+
type="radio"
187+
name="brand-customize-bg"
188+
:value="true"
189+
class="sr-only"
190+
/>
192191
{{ $t('brand.logos.on_dark') }}
193-
</ButtonBase>
194-
<ButtonBase
195-
size="md"
196-
role="radio"
197-
:aria-checked="!customBgDark"
198-
:aria-label="$t('brand.logos.on_light')"
199-
class="!border-none !rounded-none border-is border-is-border motion-reduce:transition-none"
200-
:class="
201-
!customBgDark
202-
? 'bg-bg-muted text-fg'
203-
: 'bg-transparent text-fg-muted hover:text-fg'
204-
"
205-
@click="customBgDark = false"
192+
</label>
193+
<label
194+
class="px-3 py-1.5 text-sm font-mono cursor-pointer border-is border-is-border motion-reduce:transition-none focus-within:bg-fg/10"
195+
:class="!customBgDark ? 'bg-bg-muted text-fg' : 'text-fg-muted hover:text-fg'"
206196
>
197+
<input
198+
v-model="customBgDark"
199+
type="radio"
200+
name="brand-customize-bg"
201+
:value="false"
202+
class="sr-only"
203+
/>
207204
{{ $t('brand.logos.on_light') }}
208-
</ButtonBase>
205+
</label>
209206
</div>
210207
</div>
211208

app/components/Readme.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,20 @@ function handleClick(event: MouseEvent) {
487487
summary {
488488
font-size: 1rem;
489489
color: var(--fg-muted);
490+
491+
/* Markdown often wraps headings/paragraphs inside <summary>, which
492+
forces them onto new lines. Inline them so the disclosure marker
493+
sits next to the label while preserving heading styles. */
494+
> h1,
495+
> h2,
496+
> h3,
497+
> h4,
498+
> h5,
499+
> h6,
500+
> p {
501+
display: inline;
502+
margin: 0;
503+
}
490504
}
491505
}
492506
</style>

app/pages/about.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import About from './about.vue'
22
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
33
import { pageDecorator } from '../../.storybook/decorators'
4-
import { contributorsHandler } from '../../.storybook/handlers'
4+
import { contributorsHandler } from '../storybook/mocks/handlers'
55

66
const meta = {
77
component: About,

app/pages/brand.vue

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,16 @@ function handleSvgDownload(src: string) {
5757
}
5858
}
5959
60-
async function handlePngDownload(logo: (typeof logos)[number]) {
61-
if (pngLoading.value.has(logo.src)) return
62-
pngLoading.value.add(logo.src)
60+
async function handlePngDownload(logo: (typeof logos)[number], variant: 'dark' | 'light' = 'dark') {
61+
const src = variant === 'light' ? (logo.srcLight ?? logo.src) : logo.src
62+
if (pngLoading.value.has(src)) return
63+
pngLoading.value.add(src)
6364
try {
64-
const blob = await svgToPng(logo.src, logo.width, logo.height)
65-
const filename = logo.src.replace(/^\//, '').replace('.svg', '.png')
65+
const blob = await svgToPng(src, logo.width, logo.height)
66+
const filename = src.replace(/^\//, '').replace('.svg', '.png')
6667
downloadFile(blob, filename)
6768
} finally {
68-
pngLoading.value.delete(logo.src)
69+
pngLoading.value.delete(src)
6970
}
7071
}
7172
</script>
@@ -224,14 +225,14 @@ async function handlePngDownload(logo: (typeof logos)[number]) {
224225
name: `${logo.name()} (${$t('brand.logos.on_light')})`,
225226
})
226227
"
227-
:disabled="pngLoading.has(logo.src)"
228-
@click="handlePngDownload(logo)"
228+
:disabled="pngLoading.has(logo.srcLight ?? logo.src)"
229+
@click="handlePngDownload(logo, 'light')"
229230
>
230231
<span
231232
class="size-[1em]"
232233
aria-hidden="true"
233234
:class="
234-
pngLoading.has(logo.src)
235+
pngLoading.has(logo.srcLight ?? logo.src)
235236
? 'i-lucide:loader-circle animate-spin'
236237
: 'i-lucide:download'
237238
"
@@ -246,10 +247,8 @@ async function handlePngDownload(logo: (typeof logos)[number]) {
246247
</div>
247248
</section>
248249

249-
<!-- Customize Section (client-only: needs DOM for accent colors + canvas export) -->
250-
<ClientOnly>
251-
<BrandCustomize />
252-
</ClientOnly>
250+
<!-- Customize Section -->
251+
<BrandCustomize />
253252

254253
<!-- Typography Section -->
255254
<section aria-labelledby="brand-typography-heading">

app/pages/pds.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Pds from './pds.vue'
22
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
33
import { pageDecorator } from '../../.storybook/decorators'
4-
import { pdsUsersHandler } from '../../.storybook/handlers'
4+
import { pdsUsersHandler } from '../storybook/mocks/handlers'
55

66
const meta = {
77
component: Pds,

app/pages/recharging.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Recharging from './recharging.vue'
22
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
33
import { pageDecorator } from '../../.storybook/decorators'
4-
import { repoStatsHandler } from '../../.storybook/handlers'
4+
import { repoStatsHandler } from '../storybook/mocks/handlers'
55

66
const meta = {
77
component: Recharging,

app/pages/settings.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Settings from './settings.vue'
22
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
33
import { userEvent, expect } from 'storybook/test'
44
import { pageDecorator } from '../../.storybook/decorators'
5-
import { i18nStatusHandler } from '../../.storybook/handlers'
5+
import { i18nStatusHandler } from '../storybook/mocks/handlers/lunaria-status'
66

77
const meta = {
88
component: Settings,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import TranslationStatus from './translation-status.vue'
2+
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
3+
import { pageDecorator } from '../../.storybook/decorators'
4+
import { i18nStatusHandler } from '../storybook/mocks/handlers/lunaria-status'
5+
6+
const meta = {
7+
component: TranslationStatus,
8+
parameters: {
9+
layout: 'fullscreen',
10+
msw: {
11+
handlers: [i18nStatusHandler],
12+
},
13+
},
14+
decorators: [pageDecorator],
15+
} satisfies Meta<typeof TranslationStatus>
16+
17+
export default meta
18+
type Story = StoryObj<typeof meta>
19+
20+
/** `/lunaria/status.json` is intercepted by MSW. Showing a variety of completion level translation statuses for a subset of locales. */
21+
export const Default: Story = {}
22+
23+
/** No API response — the fetch never succeeds so `fetchStatus` stays as `'pending'`. Shows skeleton blocks in the locale list and skeleton inlines in body text. */
24+
export const WithoutTranslationData: Story = {
25+
parameters: {
26+
msw: {
27+
handlers: [],
28+
},
29+
},
30+
}

app/pages/translation-status.vue

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ defineOgImage(
1717
{ alt: () => `${$t('translation_status.title')} — npmx` },
1818
)
1919
20-
const nuxt = useNuxtApp()
2120
const router = useRouter()
2221
const canGoBack = useCanGoBack()
2322
const { fetchStatus, status } = useI18nStatus()
@@ -28,14 +27,7 @@ const isLoading = computed<boolean>(
2827
() => fetchStatus.value === 'idle' || fetchStatus.value === 'pending',
2928
)
3029
31-
const generatedAt = computed(() => {
32-
const gat = status.value?.generatedAt
33-
if (import.meta.client) {
34-
return (nuxt.isHydrated ? new Date().toISOString() : gat) ?? new Date().toISOString()
35-
}
36-
37-
return gat ?? new Date().toISOString()
38-
})
30+
const generatedAt = computed(() => status.value?.generatedAt)
3931
4032
const localeEntries = computed<I18nLocaleStatus[]>(() => status.value?.locales || [])
4133
@@ -68,16 +60,16 @@ ${template}`
6860
<span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
6961
</button>
7062
</div>
71-
<i18n-t
72-
keypath="translation_status.generated_at"
73-
tag="p"
74-
scope="global"
75-
class="text-fg-muted text-lg"
76-
>
77-
<template #date>
78-
<NuxtTime :locale :datetime="generatedAt" date-style="long" time-style="medium" />
63+
<p class="text-fg-muted text-lg">
64+
<template v-if="isLoading || !generatedAt">
65+
<SkeletonInline class="h-6 w-96" />
7966
</template>
80-
</i18n-t>
67+
<i18n-t v-else keypath="translation_status.generated_at" tag="span" scope="global">
68+
<template #date>
69+
<NuxtTime :locale :datetime="generatedAt" date-style="long" time-style="medium" />
70+
</template>
71+
</i18n-t>
72+
</p>
8173
</header>
8274

8375
<p class="text-fg-muted leading-relaxed mb-4">

0 commit comments

Comments
 (0)