Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ file-tree-sprite.svg

# output
.vercel

*storybook.log
storybook-static
88 changes: 88 additions & 0 deletions .storybook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Why Storybook?

Storybook is a development environment for UI components that helps catch UI changes and provides integrations for various testing types. For testing, Storybook offers:

- **Accessibility tests** - Built-in a11y checks
- **Visual tests** - Compare JPG screenshots
- **Vitest tests** - Use stories directly in your unit tests

## Component Categories

The plan is to organize components into 3 categories.

### UI Library Components

Generic and reusable components used throughout your application.

- Examples: Button, Input, Modal, Card
- **Testing focus:** Props, variants, accessibility
- **Coverage:** All variants and states

### Composite Components

Single-use components that encapsulate one feature.

- Examples: UserProfile, WeeklyDownloadStats
- **Testing focus:** Integration patterns, user interactions
- **Coverage:** Common usage scenarios

### Page Components

**Full-page layouts** should match what the users see.

- Examples: HomePage, Dashboard, CheckoutPage
- **Testing focus:** Layout, responsive behavior, integration testing
- **Coverage:** Critical user flows and breakpoints

## Coverage Guidelines

### Which Components Need Stories?

TBD

## Project Conventions

### Place a `.stories.ts` file next to your component

```
components/
├── Button.vue
└── Button.stories.ts
```

### Story Template

```ts
// *.stories.ts
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import Component from './Button.vue'

const meta = {
component: Component,
// component scope configuration goes here
} satisfies Meta<typeof Component>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
// story scope configuration goes here
}
```

## Configuration

Stories can be configured at three levels:

- **Global scope** (`.storybook/preview.ts`) - Applies to all stories
- **Component scope** - Applies to all stories for a specific component
- **Story scope** - Applies to individual stories only

## Global App Settings

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.

## Known Limitations

- `autodocs` usage is discouraged as it is buggy.
- Changing `i18n` in the toolbar doesn't update the language. A manual story reload is required.
11 changes: 11 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { StorybookConfig } from '@nuxtjs/storybook'

const config = {
stories: ['../app/**/*.stories.@(js|ts|mdx)'],
addons: ['@storybook/addon-a11y', '@storybook/addon-docs'],
framework: '@storybook-vue/nuxt',
features: {
backgrounds: false,
},
} satisfies StorybookConfig
export default config
Comment on lines +1 to +11
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check package.json for the dependency
echo "=== Checking package.json for `@storybook-vue/nuxt` ==="
if [ -f "package.json" ]; then
  jq '.devDependencies["@storybook-vue/nuxt"] // .dependencies["@storybook-vue/nuxt"] // "NOT FOUND"' package.json
else
  echo "package.json not found"
fi

echo ""
echo "=== Checking `@nuxtjs/storybook` dependency ==="
if [ -f "package.json" ]; then
  jq '.devDependencies["@nuxtjs/storybook"] // .dependencies["@nuxtjs/storybook"] // "NOT FOUND"' package.json
fi

echo ""
echo "=== Searching for knip configuration ==="
find . -type f \( -name "knip.json" -o -name "knip.ts" -o -name "knip.js" \) 2>/dev/null | head -10
if [ $? -eq 0 ]; then
  fd -t f -e json -e ts -e js -path "*knip*" --exec cat {} \; 2>/dev/null | head -100
fi

echo ""
echo "=== Checking package.json for knip config ==="
if [ -f "package.json" ]; then
  jq '.knip // "NOT FOUND"' package.json
fi

Repository: npmx-dev/npmx.dev

Length of output: 293


🏁 Script executed:

#!/bin/bash

echo "=== Contents of knip.ts ==="
if [ -f "knip.ts" ]; then
  cat -n knip.ts
fi

echo ""
echo "=== Checking if `@storybook-vue/nuxt` appears in knip ignoreDependencies ==="
if [ -f "knip.ts" ]; then
  grep -i "storybook-vue\|ignoreDependencies\|ignoreWorkspaces" knip.ts
fi

echo ""
echo "=== Checking node_modules for `@storybook-vue/nuxt` ==="
if [ -d "node_modules/@storybook-vue" ]; then
  ls -la node_modules/@storybook-vue/
else
  echo "@storybook-vue directory not found in node_modules (expected in sandbox)"
fi

echo ""
echo "=== Checking `@nuxtjs/storybook` package.json for peer dependencies ==="
if [ -f "node_modules/@nuxtjs/storybook/package.json" ]; then
  jq '.peerDependencies["@storybook-vue/nuxt"] // .dependencies["@storybook-vue/nuxt"] // "NOT FOUND in peer or direct dependencies"' node_modules/@nuxtjs/storybook/package.json
else
  echo "@nuxtjs/storybook package.json not found in node_modules"
fi

Repository: npmx-dev/npmx.dev

Length of output: 2964


🏁 Script executed:

#!/bin/bash

echo "=== Checking for lock files ==="
ls -la package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null || echo "No standard lock files found in root"

echo ""
echo "=== Checking .storybook directory structure ==="
find .storybook -type f -name "*.ts" -o -name "*.js" -o -name "*.json" 2>/dev/null | head -20

echo ""
echo "=== Checking if there's a .storybook/package.json or similar ==="
if [ -f ".storybook/package.json" ]; then
  cat .storybook/package.json
fi

echo ""
echo "=== Checking if `@storybook-vue/nuxt` appears anywhere in config files ==="
rg "@storybook-vue/nuxt" --type ts --type js --type json -l

Repository: npmx-dev/npmx.dev

Length of output: 425


🏁 Script executed:

#!/bin/bash

echo "=== Searching pnpm-lock.yaml for `@storybook-vue/nuxt` ==="
grep -c "@storybook-vue/nuxt" pnpm-lock.yaml 2>/dev/null && echo "Found in lock file" || echo "Not found in lock file"

echo ""
echo "=== Checking if `@storybook-vue` is mentioned at all in lock file ==="
grep "@storybook-vue" pnpm-lock.yaml | head -5

echo ""
echo "=== Checking `@nuxtjs/storybook` in lock file for dependencies on `@storybook-vue/nuxt` ==="
rg "/@nuxtjs/storybook" -A 10 pnpm-lock.yaml | head -30

Repository: npmx-dev/npmx.dev

Length of output: 2355


Add @storybook-vue/nuxt to knip's ignoreDependencies or explicitly to devDependencies.

The dependency @storybook-vue/nuxt is installed transitively (present in pnpm-lock.yaml), but knip reports it as unlisted because it's not declared in package.json and is not ignored in the knip configuration. Add it to the ignoreDependencies array in knip.ts at line 37, or add it explicitly to devDependencies in package.json.

🧰 Tools
🪛 GitHub Actions: ci

[error] 1-1: Unlisted dependencies: @storybook-vue/nuxt. knip step failed with exit code 1 (ELIFECYCLE).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yannbf Is this relevant?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

111 changes: 111 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Preview } from '@nuxtjs/storybook'
import { currentLocales } from '../config/i18n'
import { fn } from 'storybook/test'
import { ACCENT_COLORS } from '../shared/utils/constants'

// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
// Stub Nuxt specific globals
// @ts-expect-error - dynamic global name
globalThis['__NUXT_COLOR_MODE__'] ??= {
preference: 'system',
value: 'dark',
getColorScheme: fn(() => 'dark'),
addColorScheme: fn(),
removeColorScheme: fn(),
}
// @ts-expect-error - dynamic global name
globalThis.defineOgImageComponent = fn()

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// Provides toolbars to switch things like theming and language
globalTypes: {
locale: {
name: 'Locale',
description: 'UI language',
defaultValue: 'en-US',
toolbar: {
icon: 'globe',
dynamicTitle: true,
items: [
// English is at the top so it's easier to reset to it
{ value: 'en-US', title: 'English (US)' },
...currentLocales
.filter(locale => locale.code !== 'en-US')
.map(locale => ({ value: locale.code, title: locale.name })),
],
},
},
accentColor: {
name: 'Accent Color',
description: 'Accent color',
toolbar: {
icon: 'paintbrush',
dynamicTitle: true,
items: [
...Object.keys(ACCENT_COLORS.light).map(color => ({
value: color,
title: color.charAt(0).toUpperCase() + color.slice(1),
})),
{ value: undefined, title: 'No Accent' },
],
},
},
theme: {
name: 'Theme',
description: 'Color mode',
defaultValue: 'dark',
toolbar: {
icon: 'moon',
dynamicTitle: true,
items: [
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
],
},
},
},
decorators: [
(story, context) => {
const { locale, theme, accentColor } = context.globals as {
locale: string
theme: string
accentColor?: string
}

// Set theme from globals
document.documentElement.setAttribute('data-theme', theme)

// Set accent color from globals
if (accentColor) {
document.documentElement.style.setProperty('--accent-color', `var(--swatch-${accentColor})`)
} else {
document.documentElement.style.removeProperty('--accent-color')
}

return {
template: '<story />',
// Set locale from globals
created() {
if (this.$i18n) {
this.$i18n.setLocale(locale)
}
},
updated() {
if (this.$i18n) {
this.$i18n.setLocale(locale)
}
},
}
},
],
}

export default preview
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
"editor.formatOnSave": true,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["./i18n/locales"],
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.vue": "${capture}.stories.ts"
}
}
112 changes: 112 additions & 0 deletions app/components/Button/Base.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import Component from './Base.vue'

const meta = {
component: Component,
tags: ['autodocs'],
} satisfies Meta<typeof Component>

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
args: {
variant: 'primary',
size: 'medium',
},
render: args => ({
components: { Component },
setup() {
return { args }
},
template: '<Component v-bind="args">Primary Button</Component>',
}),
}

export const Secondary: Story = {
args: {
variant: 'secondary',
size: 'medium',
},
render: args => ({
components: { Component },
setup() {
return { args }
},
template: '<Component v-bind="args">Secondary Button</Component>',
}),
}

export const Small: Story = {
args: {
variant: 'secondary',
size: 'small',
},
render: args => ({
components: { Component },
setup() {
return { args }
},
template: '<Component v-bind="args">Small Button</Component>',
}),
}

export const Disabled: Story = {
args: {
variant: 'primary',
size: 'medium',
disabled: true,
},
render: args => ({
components: { Component },
setup() {
return { args }
},
template: '<Component v-bind="args">Disabled Button</Component>',
}),
}

export const WithIcon: Story = {
args: {
variant: 'secondary',
size: 'medium',
classicon: 'i-carbon:search',
},
render: args => ({
components: { Component },
setup() {
return { args }
},
template: '<Component v-bind="args">Search</Component>',
}),
}

export const WithKeyboardShortcut: Story = {
args: {
variant: 'secondary',
size: 'medium',
ariaKeyshortcuts: '/',
},
render: args => ({
components: { Component },
setup() {
return { args }
},
template: '<Component v-bind="args">Search</Component>',
}),
}

export const Block: Story = {
args: {
variant: 'primary',
size: 'medium',
block: true,
},
render: args => ({
components: { Component },
setup() {
return { args }
},
template: '<Component v-bind="args">Full Width Button</Component>',
}),
}
17 changes: 16 additions & 1 deletion app/components/Button/Base.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
/** Whether the button is disabled */
disabled?: boolean
/**
* HTML button type attribute
* @default "button"
*/
type?: 'button' | 'submit'
/**
* Button visual style variant
* @default "secondary"
*/
variant?: 'primary' | 'secondary'
/**
* Button size
* @default "medium"
*/
size?: 'small' | 'medium'
/** Keyboard shortcut hint */
ariaKeyshortcuts?: string
/** Whether the button should take full width */
block?: boolean
/** Icon class (e.g., i-carbon-add) */
classicon?: string
}>(),
{
Expand Down
Loading
Loading