mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 02:29:58 -05:00
🎨 Storybook docs for block, card, colorpicker, formerror (#6874)
* storybook docs for block, card, colorpicker, formerror * release notes
This commit is contained in:
@@ -26,6 +26,7 @@ const config: StorybookConfig = {
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
staticDirs: ['./public'],
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig } = await import('vite');
|
||||
|
||||
|
||||
9
packages/component-library/.storybook/public/_headers
Normal file
9
packages/component-library/.storybook/public/_headers
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
# /assets folder contain processed assets with a file hash
|
||||
# They are safe for immutable caching, as filename change when content change
|
||||
|
||||
/assets/*
|
||||
Cache-Control: public
|
||||
Cache-Control: max-age=365000000
|
||||
Cache-Control: immutable
|
||||
|
||||
139
packages/component-library/src/AlignedText.stories.tsx
Normal file
139
packages/component-library/src/AlignedText.stories.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { AlignedText } from './AlignedText';
|
||||
|
||||
const meta = {
|
||||
title: 'AlignedText',
|
||||
component: AlignedText,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof AlignedText>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
left: 'Label',
|
||||
right: 'Value',
|
||||
style: { width: 300, display: 'flex' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'AlignedText displays two pieces of content aligned on opposite sides.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TruncateLeft: Story = {
|
||||
args: {
|
||||
left: 'This is a very long label that should be truncated on the left side',
|
||||
right: '$100.00',
|
||||
truncate: 'left',
|
||||
style: { width: 250, display: 'flex' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'When `truncate="left"`, the left content is truncated with ellipsis.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TruncateRight: Story = {
|
||||
args: {
|
||||
left: 'Short Label',
|
||||
right:
|
||||
'This is a very long value that should be truncated on the right side',
|
||||
truncate: 'right',
|
||||
style: { width: 250, display: 'flex' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'When `truncate="right"`, the right content is truncated with ellipsis.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FinancialAmount: Story = {
|
||||
args: {
|
||||
left: 'Groceries',
|
||||
right: '$1,234.56',
|
||||
style: { width: 300, display: 'flex' },
|
||||
rightStyle: { fontWeight: 'bold' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Example showing AlignedText used for displaying financial data.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomStyles: Story = {
|
||||
args: {
|
||||
left: 'Category',
|
||||
right: 'Amount',
|
||||
style: {
|
||||
width: 300,
|
||||
padding: 10,
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
},
|
||||
leftStyle: { color: '#666', fontStyle: 'italic' },
|
||||
rightStyle: { color: '#333', fontWeight: 'bold' },
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleRows: Story = {
|
||||
args: {
|
||||
left: 'Income',
|
||||
right: '$5,000.00',
|
||||
},
|
||||
render: () => (
|
||||
<div
|
||||
style={{ width: 300, display: 'flex', flexDirection: 'column', gap: 8 }}
|
||||
>
|
||||
<AlignedText
|
||||
left="Income"
|
||||
right="$5,000.00"
|
||||
rightStyle={{ color: 'green' }}
|
||||
style={{ display: 'flex' }}
|
||||
/>
|
||||
<AlignedText
|
||||
left="Expenses"
|
||||
right="-$3,200.00"
|
||||
rightStyle={{ color: 'red' }}
|
||||
style={{ display: 'flex' }}
|
||||
/>
|
||||
<AlignedText
|
||||
left="Balance"
|
||||
right="$1,800.00"
|
||||
style={{ borderTop: '1px solid #ccc', paddingTop: 8, display: 'flex' }}
|
||||
rightStyle={{ fontWeight: 'bold' }}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Multiple AlignedText components stacked to create a summary view.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
111
packages/component-library/src/Block.stories.tsx
Normal file
111
packages/component-library/src/Block.stories.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { Block } from './Block';
|
||||
import { theme } from './theme';
|
||||
|
||||
const meta = {
|
||||
title: 'Block',
|
||||
component: Block,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof Block>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'This is a Block component',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Block is a basic div wrapper that accepts Emotion CSS styles via the `style` prop.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export const WithStyles: Story = {
|
||||
args: {
|
||||
children: 'Styled Block',
|
||||
style: {
|
||||
padding: 20,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFlexLayout: Story = {
|
||||
render: () => (
|
||||
<Block
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 10,
|
||||
padding: 15,
|
||||
borderRadius: 4,
|
||||
color: theme.pageText,
|
||||
}}
|
||||
>
|
||||
<Block
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
Item 1
|
||||
</Block>
|
||||
<Block
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
Item 2
|
||||
</Block>
|
||||
<Block
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
Item 3
|
||||
</Block>
|
||||
</Block>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Block components can be nested and styled with flexbox.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AsContainer: Story = {
|
||||
args: {
|
||||
children: 'Container Block',
|
||||
style: {
|
||||
width: 300,
|
||||
padding: 25,
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.cardBackground,
|
||||
border: `2px dashed ${theme.cardBorder}`,
|
||||
borderRadius: 8,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -20,7 +20,6 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
|
||||
82
packages/component-library/src/Card.stories.tsx
Normal file
82
packages/component-library/src/Card.stories.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { Card } from './Card';
|
||||
import { Paragraph } from './Paragraph';
|
||||
import { theme } from './theme';
|
||||
|
||||
const meta = {
|
||||
title: 'Card',
|
||||
component: Card,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Card>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Card content goes here',
|
||||
style: {
|
||||
padding: 20,
|
||||
width: 300,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Default Card component uses the following theme CSS variables:
|
||||
- \`--color-cardBackground\`
|
||||
- \`--color-cardBorder\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomContent: Story = {
|
||||
args: {
|
||||
style: {
|
||||
padding: 20,
|
||||
width: 300,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
render: args => (
|
||||
<Card {...args}>
|
||||
<h3 style={{ ...styles.largeText }}>Card Title</h3>
|
||||
<Paragraph style={{ margin: 0 }}>
|
||||
This is a card with more complex content including a title and
|
||||
paragraph.
|
||||
</Paragraph>
|
||||
</Card>
|
||||
),
|
||||
};
|
||||
|
||||
export const Narrow: Story = {
|
||||
args: {
|
||||
children: 'Narrow card',
|
||||
style: {
|
||||
padding: 15,
|
||||
width: 150,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Wide: Story = {
|
||||
args: {
|
||||
children: 'Wide card with more content space',
|
||||
style: {
|
||||
padding: 25,
|
||||
width: 500,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
108
packages/component-library/src/ColorPicker.stories.tsx
Normal file
108
packages/component-library/src/ColorPicker.stories.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState } from 'react';
|
||||
import { ColorSwatch } from 'react-aria-components';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
|
||||
const meta = {
|
||||
title: 'ColorPicker',
|
||||
component: ColorPicker,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: {
|
||||
onChange: fn(),
|
||||
children: <Button>Pick a color</Button>,
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof ColorPicker>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
defaultValue: '#690CB0',
|
||||
children: <Button>Pick a color</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithColorSwatch: Story = {
|
||||
args: {
|
||||
defaultValue: '#1976D2',
|
||||
children: (
|
||||
<Button style={{ padding: 4 }}>
|
||||
<ColorSwatch
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
boxShadow: 'inset 0 0 0 1px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomColorSet: Story = {
|
||||
args: {
|
||||
defaultValue: '#FF0000',
|
||||
columns: 4,
|
||||
colorset: [
|
||||
'#FF0000',
|
||||
'#00FF00',
|
||||
'#0000FF',
|
||||
'#FFFF00',
|
||||
'#FF00FF',
|
||||
'#00FFFF',
|
||||
'#FFA500',
|
||||
'#800080',
|
||||
],
|
||||
children: <Button>Custom Colors</Button>,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'ColorPicker with a custom color set and different column layout.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Controlled: Story = {
|
||||
args: {
|
||||
children: <Button>Pick a color</Button>,
|
||||
},
|
||||
render: () => {
|
||||
const [color, setColor] = useState('#388E3C');
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<ColorPicker value={color} onChange={c => setColor(c.toString('hex'))}>
|
||||
<Button style={{ padding: 4 }}>
|
||||
<ColorSwatch
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</ColorPicker>
|
||||
<span>Selected: {color}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Controlled ColorPicker with external state management.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
90
packages/component-library/src/FormError.stories.tsx
Normal file
90
packages/component-library/src/FormError.stories.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { FormError } from './FormError';
|
||||
import { Input } from './Input';
|
||||
import { View } from './View';
|
||||
|
||||
const meta = {
|
||||
title: 'FormError',
|
||||
component: FormError,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof FormError>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'This field is required',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'FormError displays validation error messages in red text.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InFormContext: Story = {
|
||||
render: () => (
|
||||
<View
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 5, width: 250 }}
|
||||
>
|
||||
<Input placeholder="Email address" style={{ borderColor: 'red' }} />
|
||||
<FormError>Please enter a valid email address</FormError>
|
||||
</View>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'FormError displayed below an input field with validation error.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleErrors: Story = {
|
||||
render: () => (
|
||||
<View style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<FormError>Password must be at least 8 characters</FormError>
|
||||
<FormError>Password must contain a number</FormError>
|
||||
<FormError>Password must contain a special character</FormError>
|
||||
</View>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Multiple FormError components for displaying several validation errors.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyle: Story = {
|
||||
args: {
|
||||
children: 'Custom styled error message',
|
||||
style: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
padding: 10,
|
||||
backgroundColor: '#ffebee',
|
||||
borderRadius: 4,
|
||||
border: '1px solid red',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongErrorMessage: Story = {
|
||||
args: {
|
||||
children:
|
||||
'This is a longer error message that explains the validation issue in more detail. Please correct the input and try again.',
|
||||
style: { maxWidth: 300 },
|
||||
},
|
||||
};
|
||||
6
upcoming-release-notes/6874.md
Normal file
6
upcoming-release-notes/6874.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Adding more components to the Storybook docs & improving storybook caching
|
||||
Reference in New Issue
Block a user