Skip to content

Commit 792a650

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/pds-landing-page
2 parents ebc78de + 488564c commit 792a650

File tree

242 files changed

+25738
-3521
lines changed

Some content is hidden

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

242 files changed

+25738
-3521
lines changed

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
#secure password, can use openssl rand -hex 32
2-
NUXT_SESSION_PASSWORD=""
2+
NUXT_SESSION_PASSWORD=""
3+
4+
#HMAC secret for image proxy URL signing, can use openssl rand -hex 32
5+
NUXT_IMAGE_PROXY_SECRET=""

.github/workflows/chromatic.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: chromatic
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
chromatic:
18+
name: 📚 Chromatic
19+
runs-on: ubuntu-24.04-arm
20+
21+
steps:
22+
- name: ☑️ Checkout
23+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
24+
with:
25+
fetch-depth: 0
26+
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
27+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
28+
29+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
30+
with:
31+
node-version: lts/*
32+
33+
- uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c
34+
name: 🟧 Install pnpm
35+
with:
36+
cache: true
37+
38+
- name: 📦 Install dependencies
39+
run: pnpm install
40+
41+
- name: 🧪 Run Chromatic Visual and Accessibility Tests
42+
uses: chromaui/action@a8ce9c58f59be5cc7090cadfc8f130fb08fcf0c3 # v15.1.0
43+
env:
44+
CHROMATIC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
45+
CHROMATIC_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
46+
CHROMATIC_SLUG: ${{ github.repository }}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,9 @@ file-tree-sprite.svg
4545

4646
# output
4747
.vercel
48+
49+
# Storybook
50+
*storybook.log
51+
storybook-static
52+
4853
.nvmrc

.storybook/main.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { StorybookConfig } from '@storybook-vue/nuxt'
2+
3+
const config = {
4+
stories: ['../app/**/*.stories.@(js|ts)'],
5+
addons: ['@storybook/addon-a11y', '@storybook/addon-docs', '@storybook/addon-themes'],
6+
framework: '@storybook-vue/nuxt',
7+
features: {
8+
backgrounds: false,
9+
},
10+
async viteFinal(config) {
11+
config.plugins ??= []
12+
13+
config.plugins.push({
14+
name: 'ignore-internals',
15+
transform(_, id) {
16+
if (id.includes('/app/pages/blog/') && id.endsWith('.md')) {
17+
return 'export default {}'
18+
}
19+
},
20+
})
21+
// Replace the built-in vue-docgen plugin with a fault-tolerant version.
22+
// vue-docgen-api can crash on components that import types from other
23+
// .vue files (it tries to parse the SFC with @babel/parser as plain TS).
24+
// This wrapper catches those errors so the build doesn't fail.
25+
const docgenPlugin = config.plugins?.find(
26+
(p): p is Extract<typeof p, { name: string }> =>
27+
!!p && typeof p === 'object' && 'name' in p && p.name === 'storybook:vue-docgen-plugin',
28+
)
29+
30+
if (docgenPlugin && 'transform' in docgenPlugin) {
31+
const hook = docgenPlugin.transform
32+
// Vite plugin hooks can be a function or an object with a `handler` property
33+
const originalFn = typeof hook === 'function' ? hook : hook?.handler
34+
if (originalFn) {
35+
const wrapped = async function (this: unknown, ...args: unknown[]) {
36+
try {
37+
return await originalFn.apply(this, args)
38+
} catch {
39+
return undefined
40+
}
41+
}
42+
if (typeof hook === 'function') {
43+
docgenPlugin.transform = wrapped as typeof hook
44+
} else if (hook) {
45+
hook.handler = wrapped as typeof hook.handler
46+
}
47+
}
48+
}
49+
50+
return config
51+
},
52+
} satisfies StorybookConfig
53+
54+
export default config

.storybook/preview.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { Preview } from '@storybook-vue/nuxt'
2+
import { withThemeByDataAttribute } from '@storybook/addon-themes'
3+
import { currentLocales } from '../config/i18n'
4+
import { fn } from 'storybook/test'
5+
import { ACCENT_COLORS } from '../shared/utils/constants'
6+
7+
// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
8+
// Stub Nuxt specific globals
9+
// @ts-expect-error - dynamic global name
10+
globalThis['__NUXT_COLOR_MODE__'] ??= {
11+
preference: 'system',
12+
value: 'dark',
13+
getColorScheme: fn(() => 'dark'),
14+
addColorScheme: fn(),
15+
removeColorScheme: fn(),
16+
}
17+
// @ts-expect-error - dynamic global name
18+
globalThis.defineOgImageComponent = fn()
19+
20+
const preview: Preview = {
21+
parameters: {
22+
controls: {
23+
matchers: {
24+
color: /(background|color)$/i,
25+
date: /Date$/i,
26+
},
27+
},
28+
},
29+
// Provides toolbars to switch things like theming and language
30+
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+
},
47+
accentColor: {
48+
name: 'Accent Color',
49+
description: 'Accent color',
50+
toolbar: {
51+
icon: 'paintbrush',
52+
dynamicTitle: true,
53+
items: [
54+
...Object.keys(ACCENT_COLORS.light).map(color => ({
55+
value: color,
56+
title: color.charAt(0).toUpperCase() + color.slice(1),
57+
})),
58+
{ value: undefined, title: 'No Accent' },
59+
],
60+
},
61+
},
62+
},
63+
decorators: [
64+
withThemeByDataAttribute({
65+
themes: {
66+
Light: 'light',
67+
Dark: 'dark',
68+
},
69+
defaultTheme: 'Dark',
70+
attributeName: 'data-theme',
71+
}),
72+
(story, context) => {
73+
const { locale, accentColor } = context.globals as {
74+
locale: string
75+
accentColor?: string
76+
}
77+
78+
// Set accent color from globals
79+
if (accentColor) {
80+
document.documentElement.style.setProperty('--accent-color', `var(--swatch-${accentColor})`)
81+
} else {
82+
document.documentElement.style.removeProperty('--accent-color')
83+
}
84+
85+
return {
86+
template: '<story />',
87+
// Set locale from globals
88+
created() {
89+
if (this.$i18n) {
90+
this.$i18n.setLocale(locale)
91+
}
92+
},
93+
updated() {
94+
if (this.$i18n) {
95+
this.$i18n.setLocale(locale)
96+
}
97+
},
98+
}
99+
},
100+
],
101+
}
102+
103+
export default preview

.vscode/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@
33
"editor.formatOnSave": true,
44
"i18n-ally.keystyle": "nested",
55
"i18n-ally.localesPaths": ["./i18n/locales"],
6-
"typescript.tsdk": "node_modules/typescript/lib"
6+
"typescript.tsdk": "node_modules/typescript/lib",
7+
"explorer.fileNesting.enabled": true,
8+
"explorer.fileNesting.patterns": {
9+
"*.vue": "${capture}.stories.ts"
10+
}
711
}

CONTRIBUTING.md

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ This focus helps guide our project decisions as a community and what we choose t
6060
- [Lighthouse performance tests](#lighthouse-performance-tests)
6161
- [End to end tests](#end-to-end-tests)
6262
- [Test fixtures (mocking external APIs)](#test-fixtures-mocking-external-apis)
63+
- [Storybook](#storybook)
64+
- [Component categories](#component-categories)
65+
- [Coverage guidelines](#coverage-guidelines)
66+
- [Project conventions](#project-conventions)
67+
- [Configuration](#configuration)
68+
- [Global app settings](#global-app-settings)
69+
- [Known limitations](#known-limitations)
6370
- [Submitting changes](#submitting-changes)
6471
- [Before submitting](#before-submitting)
6572
- [Pull request process](#pull-request-process)
@@ -172,10 +179,11 @@ shared/ # Shared between app and server
172179
└── types/ # TypeScript type definitions
173180
174181
cli/ # Local connector CLI (separate workspace)
182+
175183
test/ # Vitest tests
176184
├── unit/ # Unit tests (*.spec.ts)
177-
── nuxt/ # Nuxt component tests
178-
tests/ # Playwright E2E tests
185+
── nuxt/ # Nuxt component tests
186+
└── e2e/ # Playwright E2E tests
179187
```
180188

181189
> [!TIP]
@@ -458,6 +466,7 @@ The following scripts help manage translation files. `en.json` is the reference
458466
| `pnpm i18n:check:fix [locale]` | Same as check, but adds missing keys to other locales with English placeholders. |
459467
| `pnpm i18n:report` | Audits translation keys against code usage in `.vue` and `.ts` files. Reports missing keys (used in code but not in locale), unused keys (in locale but not in code), and dynamic keys. |
460468
| `pnpm i18n:report:fix` | Removes unused keys from `en.json` and all other locale files. |
469+
| `pnpm i18n:schema` | Generates a JSON Schema from `en.json` at `i18n/schema.json`. Locale files reference this schema for IDE validation and autocompletion. |
461470

462471
### Adding a new locale
463472

@@ -885,6 +894,117 @@ The mock connector supports test endpoints for state manipulation:
885894
- `/__test__/user-packages` - Set user's packages
886895
- `/__test__/package` - Set package collaborators
887896

897+
## Storybook
898+
899+
Storybook is a development environment for UI components that helps catch UI changes and provides integrations for various testing types. For testing, Storybook offers:
900+
901+
- **Accessibility tests** - Built-in a11y checks
902+
- **Visual tests** - Compare JPG screenshots
903+
- **Vitest tests** - Use stories directly in the unit tests
904+
905+
### Component categories
906+
907+
The plan is to organize components into 3 categories.
908+
909+
#### UI Library Components
910+
911+
Generic and reusable components used throughout the application.
912+
913+
- Examples: Button, Input, Modal, Card
914+
- **Testing focus:** Props, variants, accessibility
915+
- **Coverage:** All variants and states
916+
917+
#### Composite Components
918+
919+
Single-use components that encapsulate one feature.
920+
921+
- Examples: UserProfile, WeeklyDownloadStats
922+
- **Testing focus:** Integration patterns, user interactions
923+
- **Coverage:** Common usage scenarios
924+
925+
#### Page Components
926+
927+
**Full-page layouts** should match what the users see.
928+
929+
- Examples: HomePage, Dashboard, CheckoutPage
930+
- **Testing focus:** Layout, responsive behavior, integration testing
931+
- **Coverage:** Critical user flows and breakpoints
932+
933+
### Coverage guidelines
934+
935+
#### Which Components Need Stories?
936+
937+
TBD
938+
939+
### Project conventions
940+
941+
#### Place `.stories.ts` files next to the component
942+
943+
```sh
944+
components/
945+
├── Button.vue
946+
└── Button.stories.ts
947+
```
948+
949+
#### Story Template
950+
951+
```ts
952+
// *.stories.ts
953+
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
954+
import Component from './Button.vue'
955+
956+
const meta = {
957+
component: Component,
958+
// component scope configuration goes here
959+
} satisfies Meta<typeof Component>
960+
961+
export default meta
962+
type Story = StoryObj<typeof meta>
963+
964+
export const Default: Story = {
965+
// story scope configuration goes here
966+
}
967+
```
968+
969+
#### JSDocs Annotation
970+
971+
The component should include descriptive comments.
972+
973+
```ts
974+
// Button.vue
975+
<script setup lang="ts">
976+
const props = withDefaults(
977+
defineProps<{
978+
/** Whether the button is disabled */
979+
disabled?: boolean
980+
/**
981+
* HTML button type attribute
982+
* @default "button"
983+
type?: 'button' | 'submit'
984+
// ...
985+
}>)
986+
</script>
987+
```
988+
989+
### Configuration
990+
991+
Stories can be configured at three levels:
992+
993+
- **Global scope** (`.storybook/preview.ts`) - Applies to all stories
994+
- **Component scope** - Applies to all stories for a specific component
995+
- **Story scope** - Applies to individual stories only
996+
997+
### Global app settings
998+
999+
Global application settings are added to the Storybook toolbar for easy testing and viewing. Configure these in `.storybook/preview.ts` under the `globalTypes` and `decorators` properties.
1000+
1001+
### Known limitations
1002+
1003+
- Changing `i18n` in the toolbar doesn't update the language. A manual story reload is required.
1004+
- `autodocs` currently is non-functional due bugs, its usage is discouraged at this time.
1005+
- `pnpm storybook` may log warnings or non-breaking errors for Nuxt modules due to the lack of mocks. If the UI renders correctly, these can be safely ignored.
1006+
- Do not `import type` from `.vue` files. The `vue-docgen-api` parser used by `@storybook/addon-docs` cannot follow type imports across SFCs and will crash. Extract shared types into a separate `.ts` file instead.
1007+
8881008
## Submitting changes
8891009
8901010
### Before submitting

0 commit comments

Comments
 (0)