Loading...
Loading...
Storybook expert for Angular 21+ component development, visual testing, and design system documentation
npx skill4agent add tidemann/st44-home agent-storybook.stories.tsapps/frontend/
├── .storybook/
│ ├── main.ts # Main configuration
│ ├── preview.ts # Global decorators & parameters
│ └── manager.ts # UI customization (optional)
├── src/
│ └── app/
│ ├── components/
│ │ └── button/
│ │ ├── button.component.ts
│ │ └── button.stories.ts
│ └── pages/
│ └── home/
│ ├── home.component.ts
│ └── home.stories.ts/Components/ButtonComponents/Forms/InputComponents/Cards/TaskCardPages/HomePages/Auth/LoginDesign System/ColorsDesign System/TypographyDesign System/SpacingExamples/Forms/LoginFormExamples/Layouts/Dashboardimport type { Meta, StoryObj } from '@storybook/angular';
import { ButtonComponent } from './button.component';
const meta: Meta<ButtonComponent> = {
title: 'Components/Button',
component: ButtonComponent,
tags: ['autodocs'], // Automatic documentation
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
description: 'Button visual style',
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
disabled: {
control: 'boolean',
},
loading: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<ButtonComponent>;
// Primary state
export const Primary: Story = {
args: {
label: 'Click me',
variant: 'primary',
size: 'medium',
},
};
// Secondary state
export const Secondary: Story = {
args: {
label: 'Click me',
variant: 'secondary',
},
};
// Disabled state
export const Disabled: Story = {
args: {
label: 'Click me',
disabled: true,
},
};
// Loading state
export const Loading: Story = {
args: {
label: 'Please wait...',
loading: true,
},
};
// All sizes comparison
export const AllSizes: Story = {
render: (args) => ({
props: args,
template: `
<div style="display: flex; gap: 1rem; align-items: center;">
<app-button size="small" [label]="label"></app-button>
<app-button size="medium" [label]="label"></app-button>
<app-button size="large" [label]="label"></app-button>
</div>
`,
}),
args: {
label: 'Click me',
},
};import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig } from '@storybook/angular';
import { provideHttpClient } from '@angular/common/http';
import { TaskCardComponent } from './task-card.component';
const meta: Meta<TaskCardComponent> = {
title: 'Components/Cards/TaskCard',
component: TaskCardComponent,
tags: ['autodocs'],
decorators: [
applicationConfig({
providers: [
provideHttpClient(),
// Add other providers here
],
}),
],
};
export default meta;
type Story = StoryObj<TaskCardComponent>;
export const Default: Story = {
args: {
task: {
id: '1',
title: 'Clean bathroom',
assignee: 'Alex',
points: 10,
completed: false,
},
},
};
export const Completed: Story = {
args: {
task: {
id: '2',
title: 'Take out trash',
assignee: 'Sarah',
points: 5,
completed: true,
},
},
};import type { Meta, StoryObj } from '@storybook/angular';
import { HomeComponent } from './home.component';
import { TaskCardComponent } from '../../components/task-card/task-card.component';
import { StatCardComponent } from '../../components/stat-card/stat-card.component';
const meta: Meta<HomeComponent> = {
title: 'Pages/Home',
component: HomeComponent,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen', // Use full viewport
},
decorators: [
applicationConfig({
providers: [
/* ... */
],
}),
],
};
export default meta;
type Story = StoryObj<HomeComponent>;
export const Default: Story = {};
export const WithManyTasks: Story = {
args: {
tasks: [
{ id: '1', title: 'Task 1' /* ... */ },
{ id: '2', title: 'Task 2' /* ... */ },
{ id: '3', title: 'Task 3' /* ... */ },
],
},
};
export const EmptyState: Story = {
args: {
tasks: [],
},
};import type { Meta, StoryObj } from '@storybook/angular';
import { signal } from '@angular/core';
import { AddChildModal } from './add-child-modal';
const meta: Meta<AddChildModal> = {
title: 'Components/Modals/AddChildModal',
component: AddChildModal,
tags: ['autodocs'],
argTypes: {
open: { control: 'boolean' },
},
// Decorators for modal positioning
decorators: [
(story) => ({
template: `
<div style="min-height: 600px; position: relative;">
<story />
</div>
`,
}),
],
};
export default meta;
type Story = StoryObj<AddChildModal>;
// Default: Modal open
export const Default: Story = {
args: {
open: true,
},
};
// Closed state
export const Closed: Story = {
args: {
open: false,
},
parameters: {
docs: {
description: {
story: 'Modal in closed state (not visible)',
},
},
},
};
// With validation errors
export const WithValidationErrors: Story = {
args: {
open: true,
},
play: async ({ canvasElement }) => {
// Simulate invalid form state for visual testing
const canvas = within(canvasElement);
const submitButton = await canvas.findByRole('button', { name: /add child/i });
await userEvent.click(submitButton);
// Form validation errors should now be visible
},
};open.claude/skills/modal-components/SKILL.mdimport type { Meta, StoryObj } from '@storybook/angular';
const meta: Meta = {
title: 'Design System/Colors',
tags: ['autodocs'],
};
export default meta;
export const Primary: StoryObj = {
render: () => ({
template: `
<div style="display: grid; gap: 1rem;">
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="width: 100px; height: 100px; background: var(--color-primary); border-radius: 8px;"></div>
<div>
<h3>Primary</h3>
<p>--color-primary: #6366F1</p>
</div>
</div>
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="width: 100px; height: 100px; background: var(--color-secondary); border-radius: 8px;"></div>
<div>
<h3>Secondary</h3>
<p>--color-secondary: #EC4899</p>
</div>
</div>
<!-- Add more colors -->
</div>
`,
}),
};
export const AllColors: StoryObj = {
render: () => ({
template: `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
<div style="background: var(--color-primary); padding: 2rem; border-radius: 8px; color: white;">
<div style="font-weight: bold;">Primary</div>
<div style="font-size: 0.875rem;">#6366F1</div>
</div>
<div style="background: var(--color-secondary); padding: 2rem; border-radius: 8px; color: white;">
<div style="font-weight: bold;">Secondary</div>
<div style="font-size: 0.875rem;">#EC4899</div>
</div>
<!-- Add all design system colors -->
</div>
`,
}),
};export const Typography: StoryObj = {
render: () => ({
template: `
<div style="font-family: var(--font-body);">
<h1 style="font-family: var(--font-heading); font-size: 48px; font-weight: 700;">
Heading 1 - Fredoka
</h1>
<h2 style="font-family: var(--font-heading); font-size: 36px; font-weight: 600;">
Heading 2 - Fredoka
</h2>
<p style="font-size: 16px;">
Body text - Outfit Regular
</p>
<p style="font-size: 16px; font-weight: 600;">
Body text - Outfit Semibold
</p>
</div>
`,
}),
};main.tsimport type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-interactions',
'@storybook/addon-links',
],
framework: {
name: '@storybook/angular',
options: {},
},
docs: {
autodocs: 'tag', // Generate docs for components with 'autodocs' tag
},
};
export default config;preview.tsimport type { Preview } from '@storybook/angular';
import { setCompodocJson } from '@storybook/addon-docs/angular';
import docJson from '../documentation.json';
setCompodocJson(docJson);
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#F8F9FF' },
{ name: 'dark', value: '#1E293B' },
{ name: 'white', value: '#FFFFFF' },
],
},
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: { width: '375px', height: '667px' },
type: 'mobile',
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
type: 'tablet',
},
desktop: {
name: 'Desktop',
styles: { width: '1440px', height: '900px' },
type: 'desktop',
},
},
},
},
};
export default preview;export const MyComponent: Story = {
parameters: {
a11y: {
config: {
rules: [
{
id: 'color-contrast',
enabled: true,
},
],
},
},
},
};import { within, userEvent } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
export const ClickButton: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await userEvent.click(button);
await expect(button).toHaveClass('clicked');
},
};# Generate component
cd apps/frontend
ng generate component components/button --standalone# Create story file
touch src/app/components/button/button.stories.ts# Run Storybook
npm run storybook
# Storybook opens at localhost:6006
# Hot reload automatically updates as you code# Start Storybook dev server
npm run storybook
# Build static Storybook
npm run build-storybook
# Generate Compodoc documentation
npm run storybook:docs
# Run both Compodoc and Storybook
npm run storybook:docs && npm run storybook# Create new story file
touch src/app/components/my-component/my-component.stories.ts
# Follow naming convention: {component-name}.stories.tspreview.ts# Regenerate documentation
npm run storybook:docs
# Check for TypeScript errors
npm run type-check# GitHub Actions example
- name: Build Storybook
run: |
cd apps/frontend
npm run storybook:docs
npm run build-storybook# Build static version
npm run build-storybook
# Deploy to static hosting (Netlify, Vercel, GitHub Pages)
# Output directory: storybook-static/