Files
AstralRinth/packages/ui/src/stories/add-stories.md
Truman Gao daf804947c devex: storybook for UI Package (#4984)
* add storybook

* clean up stories

* small fix

* add stories for all components

* add vintl

* default to dark mode

* fix  teleport

* add theme addon

* add new modal story

* delete broken stories

* move all stories to central stories folder

* fix paths

* add pnpm run storybook

* remove chromatic

* add add-stories.md

* fix types

* fix unncessary args field

* cover more addordion states

* pt2

* remove old vintl

* fix: missing style + ctx

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-01-02 00:32:58 +00:00

6.8 KiB

Storybook Story Creation Guide

This document provides instructions for AI assistants when creating Storybook stories for Vue components in the @modrinth/ui package.

File Location

Stories should be placed in a stories subdirectory next to the component's directory:

  • Component: src/components/base/MyComponent.vue
  • Story: src/stories/base/MyComponent.stories.ts

Example with modal components:

  • Component: src/components/modal/MyModal.vue
  • Story: src/stories/modal/MyModal.stories.ts

Basic Story Structure

import type { Meta, StoryObj } from '@storybook/vue3-vite'
import MyComponent from '../../components/base/MyComponent.vue'

const meta = {
	title: 'Base/MyComponent', // Category/ComponentName
	component: MyComponent,
} satisfies Meta<typeof MyComponent>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
	args: {
		// Default prop values
	},
}

Key Principles

1. Let Storybook Auto-Infer Props

DO NOT manually define argTypes.

// ❌ BAD - Don't include prop types
const meta = {
	argTypes: {
		size: { control: 'select', options: ['small', 'medium', 'large'] },
	},
}

// ✅ GOOD - Let Storybook infer from component
const meta = {
	component: MyComponent,
}

2. Use Render Functions for Slot Content

When a component uses slots, provide a render function:

const meta = {
	component: Accordion,
	render: (args) => ({
		components: { Accordion },
		setup() {
			return { args }
		},
		template: /* html */ `
            <Accordion v-bind="args">
                <template #title>Click to expand</template>
                <p>Accordion content here.</p>
            </Accordion>
        `,
	}),
} satisfies Meta<typeof Accordion>

3. Keep Stories Concise

Instead of creating individual stories for every prop variant, use showcase stories:

Make sure to type it as StoryObj when using render functions.

// ❌ BAD - Too many individual stories
export const Small: Story = { args: { size: 'small' } }
export const Medium: Story = { args: { size: 'medium' } }
export const Large: Story = { args: { size: 'large' } }

// ✅ GOOD - One showcase story
export const AllSizes: StoryObj = {
	render: () => ({
		components: { MyComponent },
		template: /* html */ `
            <div class="flex gap-4">
                <MyComponent size="small">Small</MyComponent>
                <MyComponent size="medium">Medium</MyComponent>
                <MyComponent size="large">Large</MyComponent>
            </div>
        `,
	}),
}

4. Required Stories

Each component should have:

  • Default - Basic usage with controls
  • All[Variants] - Showcase stories for major prop variations (e.g., AllColors, AllSizes, AllTypes)

5. Handling Generic Vue Components

For components with generics like MyComponent<T>, add // @ts-ignore

const meta = {
	title: 'Base/Combobox',
	// @ts-ignore
	component: Combobox,
} satisfies Meta<typeof Combobox>

Common Patterns

Components with Icons

Import icons from @modrinth/assets:

import { SearchIcon, ChevronDownIcon } from '@modrinth/assets'

export const WithIcon: Story = {
	render: () => ({
		components: { MyComponent, SearchIcon },
		template: /* html */ `
            <MyComponent>
                <SearchIcon />
                Search
            </MyComponent>
        `,
	}),
	args: {},
}

Interactive Components (Modals, Dropdowns)

For components that need user interaction to show:

export const Default: Story = {
	render: () => ({
		components: { Modal, ButtonStyled },
		setup() {
			const modalRef = ref<InstanceType<typeof Modal> | null>(null)
			return { modalRef }
		},
		template: /* html */ `
            <div>
                <ButtonStyled @click="modalRef?.show()">
                    <button>Open Modal</button>
                </ButtonStyled>
                <Modal ref="modalRef" header="Example Modal">
                    <p>Modal content</p>
                </Modal>
            </div>
        `,
	}),
	args: {},
}

Components with v-model

export const Default: Story = {
	render: () => ({
		components: { Toggle },
		setup() {
			const value = ref(false)
			return { value }
		},
		template: /* html */ `
            <Toggle v-model="value" />
        `,
	}),
	args: {},
}

Things to Avoid

1. Don't Import from @modrinth/ui in Components

Components should use relative imports, not the package alias:

// ❌ BAD - Causes circular dependency in Storybook
import { ButtonStyled } from '@modrinth/ui'

// ✅ GOOD - Use relative imports
import ButtonStyled from '../base/ButtonStyled.vue'

2. Object/Array Prop Defaults Must Be Factory Functions

This is a Vue requirement that vue-docgen-plugin enforces:

// ❌ BAD - Will cause Storybook build error
defineProps<{ icon: Component }>()
withDefaults(defineProps<{ icon: Component }>(), {
	icon: TrashIcon,
})

// ✅ GOOD - Use factory function
withDefaults(defineProps<{ icon: Component }>(), {
	icon: () => TrashIcon,
})

Dependencies Available in Storybook

The following are configured in .storybook/preview.ts:

  • VIntl: useVIntl() and formatMessage() work automatically
  • Teleports: <Teleport to="#teleports"> has a target element
  • Tailwind CSS: All Tailwind classes are available
  • Dark Mode: Use @storybook/addon-themes for theme switching

Example: Complete Story File

import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import Badge from '../../components/base/Badge.vue'

const meta = {
	title: 'Base/Badge',
	component: Badge,
	render: (args) => ({
		components: { Badge },
		setup() {
			return { args }
		},
		template: /* html */ `
            <Badge v-bind="args">Badge Text</Badge>
        `,
	}),
} satisfies Meta<typeof Badge>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
	args: {
		color: 'green',
	},
}

export const AllColors: StoryObj = {
	render: () => ({
		components: { Badge },
		template: /* html */ `
            <div class="flex flex-wrap gap-2">
                <Badge color="green">Green</Badge>
                <Badge color="red">Red</Badge>
                <Badge color="orange">Orange</Badge>
                <Badge color="blue">Blue</Badge>
                <Badge color="purple">Purple</Badge>
                <Badge color="gray">Gray</Badge>
            </div>
        `,
	}),
}

export const AllTypes: StoryObj = {
	render: () => ({
		components: { Badge },
		template: /* html */ `
            <div class="flex flex-wrap gap-2">
                <Badge type="default">Default</Badge>
                <Badge type="outlined">Outlined</Badge>
                <Badge type="highlight">Highlight</Badge>
            </div>
        `,
	}),
}