mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 06:02:22 -05:00
🎨 Storybook docs (#6770)
* start to a storybook * release notes * commit the release notes * add test:web back * adding some scripts to the main package for storybook * styled it up a bit * remove unneeded icon * lint * remove needless comment * moving clarifying comment * fast failing * feedback * removing unneeded config
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -76,3 +76,6 @@ build/
|
||||
|
||||
# Lage cache
|
||||
.lage/
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"start:service-plugins": "yarn workspace plugins-service watch",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"start:storybook": "yarn workspace @actual-app/components start:storybook",
|
||||
"build:browser-backend": "yarn workspace loot-core build:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
@@ -40,6 +41,7 @@
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"build:docs": "yarn workspace docs build",
|
||||
"build:storybook": "yarn workspace @actual-app/components build:storybook",
|
||||
"deploy:docs": "yarn workspace docs deploy",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||
|
||||
3
packages/component-library/.oxlintrc.json
Normal file
3
packages/component-library/.oxlintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"jsPlugins": ["eslint-plugin-storybook"]
|
||||
}
|
||||
43
packages/component-library/.storybook/main.ts
Normal file
43
packages/component-library/.storybook/main.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string) {
|
||||
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
'../src/Introduction.mdx',
|
||||
'../src/**/*.mdx',
|
||||
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
getAbsolutePath('@chromatic-com/storybook'),
|
||||
getAbsolutePath('@storybook/addon-a11y'),
|
||||
getAbsolutePath('@storybook/addon-docs'),
|
||||
],
|
||||
framework: getAbsolutePath('@storybook/react-vite'),
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig } = await import('vite');
|
||||
|
||||
return mergeConfig(config, {
|
||||
// Telling Vite how to resolve path aliases
|
||||
plugins: [viteTsconfigPaths({ root: '../..' })],
|
||||
esbuild: {
|
||||
// Needed to handle JSX in .ts/.tsx files
|
||||
jsx: 'automatic',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
74
packages/component-library/.storybook/manager.ts
Normal file
74
packages/component-library/.storybook/manager.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { addons } from 'storybook/manager-api';
|
||||
import { create } from 'storybook/theming/create';
|
||||
|
||||
// Colors from the Actual Budget light theme palette
|
||||
const purple500 = '#8719e0';
|
||||
const purple400 = '#9a3de8';
|
||||
const navy900 = '#102a43';
|
||||
const navy700 = '#334e68';
|
||||
const navy600 = '#486581';
|
||||
const navy150 = '#d9e2ec';
|
||||
const navy100 = '#e8ecf0';
|
||||
const white = '#ffffff';
|
||||
|
||||
// Create a custom Storybook theme matching Actual Budget's light theme
|
||||
const theme = create({
|
||||
base: 'light',
|
||||
brandTitle: 'Actual Budget',
|
||||
brandUrl: 'https://actualbudget.org',
|
||||
brandImage: 'https://actualbudget.org/img/actual.webp',
|
||||
brandTarget: '_blank',
|
||||
|
||||
// UI colors
|
||||
colorPrimary: purple500,
|
||||
colorSecondary: purple400,
|
||||
|
||||
// App chrome
|
||||
appBg: navy100,
|
||||
appContentBg: white,
|
||||
appPreviewBg: white,
|
||||
appBorderColor: navy150,
|
||||
appBorderRadius: 4,
|
||||
|
||||
// Fonts
|
||||
fontBase:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
fontCode: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
|
||||
|
||||
// Text colors
|
||||
textColor: navy900,
|
||||
textInverseColor: white,
|
||||
textMutedColor: navy600,
|
||||
|
||||
// Toolbar
|
||||
barTextColor: navy700,
|
||||
barHoverColor: purple500,
|
||||
barSelectedColor: purple500,
|
||||
barBg: white,
|
||||
|
||||
// Form colors
|
||||
buttonBg: white,
|
||||
buttonBorder: navy900,
|
||||
booleanBg: navy150,
|
||||
booleanSelectedBg: purple500,
|
||||
inputBg: white,
|
||||
inputBorder: navy900,
|
||||
inputTextColor: navy900,
|
||||
inputBorderRadius: 4,
|
||||
});
|
||||
|
||||
addons.setConfig({
|
||||
theme,
|
||||
enableShortcuts: true,
|
||||
isFullscreen: false,
|
||||
isToolshown: true,
|
||||
sidebar: {
|
||||
collapsedRoots: [],
|
||||
filters: {
|
||||
patterns: item => {
|
||||
// Hide stories that are marked as internal
|
||||
return !item.tags?.includes('internal');
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
88
packages/component-library/.storybook/preview.tsx
Normal file
88
packages/component-library/.storybook/preview.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import type { Preview } from '@storybook/react-vite';
|
||||
|
||||
// Not ideal to import from desktop-client, but we need a source of truth for theme variables
|
||||
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
|
||||
import * as developmentTheme from '../../desktop-client/src/style/themes/development';
|
||||
import * as lightTheme from '../../desktop-client/src/style/themes/light';
|
||||
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
|
||||
|
||||
const THEMES = {
|
||||
light: lightTheme,
|
||||
dark: darkTheme,
|
||||
midnight: midnightTheme,
|
||||
development: developmentTheme,
|
||||
} as const;
|
||||
|
||||
type ThemeName = keyof typeof THEMES;
|
||||
|
||||
const ThemedStory = ({
|
||||
themeName,
|
||||
children,
|
||||
}: {
|
||||
themeName?: ThemeName;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
if (!themeName || !THEMES[themeName]) {
|
||||
throw new Error(`No theme specified`);
|
||||
}
|
||||
|
||||
const css = Object.entries(THEMES[themeName])
|
||||
.map(([key, value]) => `--color-${key}: ${value};`)
|
||||
.join('\n');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<style>{`:root {\n${css}}`}</style>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story, { globals }) => {
|
||||
const themeName = globals.theme;
|
||||
|
||||
return (
|
||||
<ThemedStory themeName={themeName}>
|
||||
<Story />
|
||||
</ThemedStory>
|
||||
);
|
||||
},
|
||||
],
|
||||
globalTypes: {
|
||||
theme: {
|
||||
name: 'Theme',
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'light',
|
||||
toolbar: {
|
||||
icon: 'circlehollow',
|
||||
items: [
|
||||
{ value: 'light', title: 'Light' },
|
||||
{ value: 'dark', title: 'Dark' },
|
||||
{ value: 'midnight', title: 'Midnight' },
|
||||
{ value: 'development', title: 'Development' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -37,7 +37,9 @@
|
||||
"scripts": {
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||
"test": "npm-run-all -cp 'test:*'",
|
||||
"test:web": "ENV=web vitest --run -c vitest.web.config.ts"
|
||||
"test:web": "ENV=web vitest --run -c vitest.web.config.ts",
|
||||
"start:storybook": "storybook dev -p 6006",
|
||||
"build:storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
@@ -45,10 +47,16 @@
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^5.0.0",
|
||||
"@storybook/addon-a11y": "^10.2.0",
|
||||
"@storybook/addon-docs": "^10.2.0",
|
||||
"@storybook/react-vite": "^10.2.0",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.2.5",
|
||||
"eslint-plugin-storybook": "^10.2.0",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"storybook": "^10.2.0",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
100
packages/component-library/src/Button.stories.ts
Normal file
100
packages/component-library/src/Button.stories.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import { Button } from './Button';
|
||||
|
||||
const meta = {
|
||||
title: 'Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
onClick: { action: 'clicked' },
|
||||
},
|
||||
args: { onClick: fn() },
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
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',
|
||||
bounce: false,
|
||||
children: 'Button Text',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Primary button variant uses the following theme CSS variables:
|
||||
- \`--color-buttonPrimaryText\`
|
||||
- \`--color-buttonPrimaryTextHover\`
|
||||
- \`--color-buttonPrimaryBackground\`
|
||||
- \`--color-buttonPrimaryBackgroundHover\`
|
||||
- \`--color-buttonPrimaryBorder\`
|
||||
- \`--color-buttonPrimaryShadow\`
|
||||
- \`--color-buttonPrimaryDisabledText\`
|
||||
- \`--color-buttonPrimaryDisabledBackground\`
|
||||
- \`--color-buttonPrimaryDisabledBorder\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Normal: Story = {
|
||||
args: {
|
||||
variant: 'normal',
|
||||
bounce: false,
|
||||
children: 'Button Text',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Normal button variant uses the following theme CSS variables:
|
||||
- \`--color-buttonNormalText\`
|
||||
- \`--color-buttonNormalTextHover\`
|
||||
- \`--color-buttonNormalBackground\`
|
||||
- \`--color-buttonNormalBackgroundHover\`
|
||||
- \`--color-buttonNormalBorder\`
|
||||
- \`--color-buttonNormalShadow\`
|
||||
- \`--color-buttonNormalSelectedText\`
|
||||
- \`--color-buttonNormalSelectedBackground\`
|
||||
- \`--color-buttonNormalDisabledText\`
|
||||
- \`--color-buttonNormalDisabledBackground\`
|
||||
- \`--color-buttonNormalDisabledBorder\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Bare: Story = {
|
||||
args: {
|
||||
variant: 'bare',
|
||||
bounce: false,
|
||||
children: 'Button Text',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Bare button variant uses the following theme CSS variables:
|
||||
- \`--color-buttonBareText\`
|
||||
- \`--color-buttonBareTextHover\`
|
||||
- \`--color-buttonBareBackground\`
|
||||
- \`--color-buttonBareBackgroundHover\`
|
||||
- \`--color-buttonBareBackgroundActive\`
|
||||
- \`--color-buttonBareDisabledText\`
|
||||
- \`--color-buttonBareDisabledBackground\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
27
packages/component-library/src/Introduction.mdx
Normal file
27
packages/component-library/src/Introduction.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
# Actual Budget Component Library
|
||||
|
||||
Welcome to the **Actual Budget Component Library**. Explore our UI components, see how they look across different themes, and learn how to use them in your code.
|
||||
|
||||
### What you can do here
|
||||
|
||||
- ✨ **Browse components** in the sidebar
|
||||
- 🎨 **Switch themes** using the toolbar above
|
||||
- 📚 **Read documentation** and see code examples
|
||||
- 🔍 **Test variations** and component states
|
||||
- ♿ **Check accessibility** compliance
|
||||
|
||||
### Getting Started
|
||||
|
||||
Select a component from the sidebar to explore its documentation, variants, and interactive controls.
|
||||
|
||||
---
|
||||
|
||||
### Useful Links
|
||||
|
||||
- [Actual Budget Website](https://actualbudget.org)
|
||||
- [Documentation](https://actualbudget.org/docs)
|
||||
- [GitHub Repository](https://github.com/actualbudget/actual)
|
||||
6
upcoming-release-notes/6770.md
Normal file
6
upcoming-release-notes/6770.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Add Storybook Documentation into the component library
|
||||
Reference in New Issue
Block a user