feat: button storybook

This commit is contained in:
Anoop M D
2025-12-24 05:30:04 +05:30
parent 3081c06964
commit c5abe4122b
15 changed files with 2923 additions and 346 deletions

2
.nvmrc
View File

@@ -1 +1 @@
v22.11.0 v22.12.0

View File

@@ -117,6 +117,18 @@ module.exports = runESMImports().then(() => defineConfig([
'no-undef': 'error' 'no-undef': 'error'
} }
}, },
{
// Storybook config files use CommonJS with __dirname and module.exports
files: ['packages/bruno-app/storybook/**/*.js'],
languageOptions: {
globals: {
...globals.node
}
},
rules: {
'no-undef': 'error'
}
},
{ {
files: ['packages/bruno-cli/**/*.js'], files: ['packages/bruno-cli/**/*.js'],
ignores: ['**/*.config.js'], ignores: ['**/*.config.js'],

2211
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,11 @@
], ],
"homepage": "https://usebruno.com", "homepage": "https://usebruno.com",
"devDependencies": { "devDependencies": {
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
"@storybook/builder-webpack5": "^10.1.10",
"@storybook/react": "^10.1.10",
"@storybook/react-webpack5": "^10.1.10",
"storybook": "^10.1.10",
"@eslint/compat": "^1.3.2", "@eslint/compat": "^1.3.2",
"@faker-js/faker": "^7.6.0", "@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0", "@jest/globals": "^29.2.0",

View File

@@ -22,6 +22,7 @@ build
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
*.log
# local env files # local env files
.env.local .env.local

View File

@@ -9,7 +9,9 @@
"preview": "rsbuild preview", "preview": "rsbuild preview",
"test": "jest", "test": "jest",
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"", "test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"" "prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"",
"storybook": "storybook dev -p 6006 --config-dir storybook",
"build-storybook": "storybook build --config-dir storybook"
}, },
"dependencies": { "dependencies": {
"@fontsource/inter": "^5.0.15", "@fontsource/inter": "^5.0.15",
@@ -63,6 +65,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"pdfjs-dist": "4.4.168", "pdfjs-dist": "4.4.168",
"platform": "^1.3.6", "platform": "^1.3.6",
"polished": "^4.3.1",
"posthog-node": "4.2.1", "posthog-node": "4.2.1",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"qs": "^6.11.0", "qs": "^6.11.0",

View File

@@ -74,26 +74,6 @@ const Wrapper = styled.div`
} }
} }
.btn-add-param {
font-size: 12px;
color: ${(props) => props.theme.textLink};
font-weight: 500;
padding: 7px 14px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 6px;
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
background: transparent;
transition: all 0.15s ease;
&:hover {
background: ${(props) => props.theme.listItem.hoverBg};
border-color: ${(props) => props.theme.textLink};
}
}
.tooltip-mod { .tooltip-mod {
font-size: 11px !important; font-size: 11px !important;
max-width: 200px !important; max-width: 200px !important;
@@ -123,65 +103,11 @@ const Wrapper = styled.div`
margin: 0; margin: 0;
} }
button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px;
color: ${(props) => props.theme.colors.text.muted};
background: transparent;
border: none;
cursor: pointer;
border-radius: 4px;
transition: color 0.15s ease, background 0.15s ease;
}
.button-container { .button-container {
padding: 12px 2px;
background: ${(props) => props.theme.bg};
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
.submit {
padding: 6px 16px;
font-size: ${(props) => props.theme.font.size.sm};
border-radius: ${(props) => props.theme.border.radius.base};
border: none;
background: ${(props) => props.theme.brand};
color: ${(props) => props.theme.bg};
cursor: pointer;
transition: opacity 0.15s ease;
&:hover {
opacity: 0.9;
}
}
.reset {
background: transparent;
padding: 6px 16px;
color: ${(props) => props.theme.brand};
&:hover {
opacity: 0.9;
}
}
.discard {
padding: 6px 16px;
font-size: ${(props) => props.theme.font.size.sm};
border-radius: ${(props) => props.theme.border.radius.base};
background: transparent;
color: ${(props) => props.theme.text};
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
}
`; `;
export default Wrapper; export default Wrapper;

View File

@@ -17,6 +17,7 @@ import {
} from 'providers/ReduxStore/slices/global-environments'; } from 'providers/ReduxStore/slices/global-environments';
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
import { getGlobalEnvironmentVariables } from 'utils/collections'; import { getGlobalEnvironmentVariables } from 'utils/collections';
import Button from 'ui/Button';
const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => { const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -425,14 +426,14 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</table> </table>
</div> </div>
<div className="button-container"> <div className="button-container mt-5">
<div className="flex items-center"> <div className="flex items-center gap-2">
<button type="button" className="submit" onClick={handleSave} data-testid="save-env"> <Button type="submit" size="sm" onClick={handleSave} data-testid="save-env">
Save Save
</button> </Button>
<button type="button" className="submit reset ml-2" onClick={handleReset} data-testid="reset-env"> <Button type="reset" size="sm" color="secondary" variant="ghost" onClick={handleReset} data-testid="reset-env">
Reset Reset
</button> </Button>
</div> </div>
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@@ -2,11 +2,13 @@ const colors = {
BRAND: '#d9a342', BRAND: '#d9a342',
TEXT: '#d4d4d4', TEXT: '#d4d4d4',
TEXT_MUTED: '#858585', TEXT_MUTED: '#858585',
TEXT_LINK: '#569cd6', TEXT_LINK: '#8BB7E0',
BG: '#1e1e1e', BG: '#1e1e1e',
GREEN: '#4ec9b0', GREEN: '#4ec9b0',
YELLOW: '#d9a342', YELLOW: '#d9a342',
WHITE: '#fff',
BLACK: '#000',
GRAY_1: '#252526', GRAY_1: '#252526',
GRAY_2: '#3D3D3D', GRAY_2: '#3D3D3D',
@@ -343,6 +345,30 @@ const darkTheme = {
border: '#dc3545' border: '#dc3545'
} }
}, },
button2: {
color: {
primary: {
bg: colors.BRAND,
text: colors.BLACK
},
secondary: {
bg: colors.GRAY_4,
text: '#fff'
},
success: {
bg: '#059669',
text: '#fff'
},
warning: {
bg: '#f59e0b',
text: '#1e1e1e'
},
danger: {
bg: '#f43f5e',
text: '#fff'
}
}
},
tabs: { tabs: {
marginRight: '1.2rem', marginRight: '1.2rem',

View File

@@ -348,7 +348,30 @@ const lightTheme = {
border: '#dc3545' border: '#dc3545'
} }
}, },
button2: {
color: {
primary: {
bg: colors.BRAND,
text: '#fff'
},
secondary: {
bg: '#e5e7eb',
text: colors.TEXT
},
success: {
bg: '#4f9a7d',
text: '#fff'
},
warning: {
bg: '#c98b2b',
text: '#fff'
},
danger: {
bg: '#d14f5b',
text: '#fff'
}
}
},
tabs: { tabs: {
marginRight: '1.2rem', marginRight: '1.2rem',
active: { active: {

View File

@@ -0,0 +1,430 @@
import React from 'react';
import Button from './index';
export default {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['xs', 'sm', 'base', 'md', 'lg'],
description: 'The size of the button'
},
variant: {
control: 'select',
options: ['filled', 'outline', 'ghost'],
description: 'The visual style variant of the button'
},
color: {
control: 'select',
options: ['primary', 'secondary', 'success', 'warning', 'danger'],
description: 'The color of the button'
},
fontWeight: {
control: 'select',
options: ['regular', 'medium'],
description: 'Font weight (default: regular for filled/ghost, medium for outline)'
},
rounded: {
control: 'select',
options: ['sm', 'md', 'full'],
description: 'Border radius style'
},
disabled: {
control: 'boolean',
description: 'Whether the button is disabled'
},
loading: {
control: 'boolean',
description: 'Whether the button is in loading state'
},
fullWidth: {
control: 'boolean',
description: 'Whether the button takes full width'
},
iconPosition: {
control: 'select',
options: ['left', 'right'],
description: 'Position of the icon relative to text'
},
children: {
control: 'text',
description: 'Button text content'
},
onClick: { action: 'clicked' },
onDoubleClick: { action: 'double-clicked' },
onMouseEnter: { action: 'mouse-entered' },
onMouseLeave: { action: 'mouse-left' }
}
};
// Sample icon component for stories
const PlusIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
);
const SendIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
);
const TrashIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
);
// Default story
export const Default = {
args: {
children: 'Button'
}
};
// Variants
export const Filled = {
render: () => (
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Button variant="filled" color="primary">Primary</Button>
<Button variant="filled" color="secondary">Secondary</Button>
<Button variant="filled" color="success">Success</Button>
<Button variant="filled" color="warning">Warning</Button>
<Button variant="filled" color="danger">Danger</Button>
</div>
)
};
export const Outline = {
render: () => (
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Button variant="outline" color="primary">Primary</Button>
<Button variant="outline" color="secondary">Secondary</Button>
<Button variant="outline" color="success">Success</Button>
<Button variant="outline" color="warning">Warning</Button>
<Button variant="outline" color="danger">Danger</Button>
</div>
)
};
export const Ghost = {
render: () => (
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Button variant="ghost" color="primary">Primary</Button>
<Button variant="ghost" color="secondary">Secondary</Button>
<Button variant="ghost" color="success">Success</Button>
<Button variant="ghost" color="warning">Warning</Button>
<Button variant="ghost" color="danger">Danger</Button>
</div>
)
};
// With Icons
export const WithIconLeft = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="filled" size="xs" icon={<PlusIcon />} iconPosition="left">Add</Button>
<Button variant="filled" size="sm" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="filled" size="base" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="filled" size="md" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="filled" size="lg" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="outline" size="xs" icon={<PlusIcon />} iconPosition="left">Add</Button>
<Button variant="outline" size="sm" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="outline" size="base" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="outline" size="md" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="outline" size="lg" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="ghost" size="xs" icon={<PlusIcon />} iconPosition="left">Add</Button>
<Button variant="ghost" size="sm" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="ghost" size="base" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="ghost" size="md" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
<Button variant="ghost" size="lg" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
</div>
</div>
</div>
)
};
export const WithIconRight = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="filled" size="xs" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="filled" size="sm" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="filled" size="base" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="filled" size="md" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="filled" size="lg" icon={<SendIcon />} iconPosition="right">Send</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="outline" size="xs" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="outline" size="sm" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="outline" size="base" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="outline" size="md" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="outline" size="lg" icon={<SendIcon />} iconPosition="right">Send</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="ghost" size="xs" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="ghost" size="sm" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="ghost" size="base" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="ghost" size="md" icon={<SendIcon />} iconPosition="right">Send</Button>
<Button variant="ghost" size="lg" icon={<SendIcon />} iconPosition="right">Send</Button>
</div>
</div>
</div>
)
};
export const IconOnly = {
args: {
icon: <PlusIcon />,
rounded: 'full',
size: 'base'
}
};
// States
export const Disabled = {
render: () => (
<div style={{ display: 'flex', gap: '48px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<h3 style={{ marginBottom: '4px', fontSize: '14px', fontWeight: 600 }}>Enabled</h3>
<Button variant="filled" color="primary">Primary</Button>
<Button variant="filled" color="secondary">Secondary</Button>
<Button variant="filled" color="success">Success</Button>
<Button variant="filled" color="warning">Warning</Button>
<Button variant="filled" color="danger">Danger</Button>
<Button variant="outline" color="primary">Outline</Button>
<Button variant="ghost" color="primary">Ghost</Button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<h3 style={{ marginBottom: '4px', fontSize: '14px', fontWeight: 600 }}>Disabled</h3>
<Button variant="filled" color="primary" disabled>Primary</Button>
<Button variant="filled" color="secondary" disabled>Secondary</Button>
<Button variant="filled" color="success" disabled>Success</Button>
<Button variant="filled" color="warning" disabled>Warning</Button>
<Button variant="filled" color="danger" disabled>Danger</Button>
<Button variant="outline" color="primary" disabled>Outline</Button>
<Button variant="ghost" color="primary" disabled>Ghost</Button>
</div>
</div>
)
};
export const Loading = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="filled" size="xs" loading>Loading</Button>
<Button variant="filled" size="sm" loading>Loading</Button>
<Button variant="filled" size="base" loading>Loading</Button>
<Button variant="filled" size="md" loading>Loading</Button>
<Button variant="filled" size="lg" loading>Loading</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="outline" size="xs" loading>Loading</Button>
<Button variant="outline" size="sm" loading>Loading</Button>
<Button variant="outline" size="base" loading>Loading</Button>
<Button variant="outline" size="md" loading>Loading</Button>
<Button variant="outline" size="lg" loading>Loading</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="ghost" size="xs" loading>Loading</Button>
<Button variant="ghost" size="sm" loading>Loading</Button>
<Button variant="ghost" size="base" loading>Loading</Button>
<Button variant="ghost" size="md" loading>Loading</Button>
<Button variant="ghost" size="lg" loading>Loading</Button>
</div>
</div>
</div>
)
};
// Rounded variants
export const RoundedSmall = {
args: {
children: 'Small Radius',
rounded: 'sm'
}
};
export const RoundedMedium = {
args: {
children: 'Medium Radius',
rounded: 'md'
}
};
export const RoundedFull = {
args: {
children: 'Pill Button',
rounded: 'full'
}
};
// Full Width
export const FullWidth = {
args: {
children: 'Full Width Button',
fullWidth: true
},
decorators: [
(Story) => (
<div style={{ width: '300px' }}>
<Story />
</div>
)
]
};
// Combined Examples
export const DangerWithIcon = {
args: {
children: 'Delete',
variant: 'filled',
color: 'danger',
icon: <TrashIcon />
}
};
// All Colors Showcase
export const AllColors = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled Variant</h3>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Button variant="filled" color="primary">Primary</Button>
<Button variant="filled" color="secondary">Secondary</Button>
<Button variant="filled" color="success">Success</Button>
<Button variant="filled" color="warning">Warning</Button>
<Button variant="filled" color="danger">Danger</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline Variant</h3>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Button variant="outline" color="primary">Primary</Button>
<Button variant="outline" color="secondary">Secondary</Button>
<Button variant="outline" color="success">Success</Button>
<Button variant="outline" color="warning">Warning</Button>
<Button variant="outline" color="danger">Danger</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost Variant</h3>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Button variant="ghost" color="primary">Primary</Button>
<Button variant="ghost" color="secondary">Secondary</Button>
<Button variant="ghost" color="success">Success</Button>
<Button variant="ghost" color="warning">Warning</Button>
<Button variant="ghost" color="danger">Danger</Button>
</div>
</div>
</div>
)
};
// All Sizes Showcase
export const AllSizes = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="filled" size="xs">Extra Small</Button>
<Button variant="filled" size="sm">Small</Button>
<Button variant="filled" size="base">Base</Button>
<Button variant="filled" size="md">Medium</Button>
<Button variant="filled" size="lg">Large</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="outline" size="xs">Extra Small</Button>
<Button variant="outline" size="sm">Small</Button>
<Button variant="outline" size="base">Base</Button>
<Button variant="outline" size="md">Medium</Button>
<Button variant="outline" size="lg">Large</Button>
</div>
</div>
<div>
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Button variant="ghost" size="xs">Extra Small</Button>
<Button variant="ghost" size="sm">Small</Button>
<Button variant="ghost" size="base">Base</Button>
<Button variant="ghost" size="md">Medium</Button>
<Button variant="ghost" size="lg">Large</Button>
</div>
</div>
</div>
)
};

View File

@@ -0,0 +1,274 @@
import styled, { css, keyframes } from 'styled-components';
import { darken, rgba } from 'polished';
const spin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const sizeStyles = {
xs: css`
padding: 0.25rem 0.5rem;
font-size: ${(props) => props.theme.font.size.xs};
gap: 0.25rem;
.button-icon {
width: 0.75rem;
height: 0.75rem;
}
.spinner-icon {
width: 0.75rem;
height: 0.75rem;
}
`,
sm: css`
padding: 0.375rem 0.75rem;
font-size: ${(props) => props.theme.font.size.sm};
gap: 0.375rem;
.button-icon {
width: 0.875rem;
height: 0.875rem;
}
.spinner-icon {
width: 0.875rem;
height: 0.875rem;
}
`,
base: css`
padding: 0.5rem 1rem;
font-size: ${(props) => props.theme.font.size.base};
gap: 0.5rem;
.button-icon {
width: 1rem;
height: 1rem;
}
.spinner-icon {
width: 1rem;
height: 1rem;
}
`,
md: css`
padding: 0.625rem 1.125rem;
font-size: ${(props) => props.theme.font.size.md};
gap: 0.5rem;
.button-icon {
width: 1rem;
height: 1rem;
}
.spinner-icon {
width: 1rem;
height: 1rem;
}
`,
lg: css`
padding: 0.75rem 1.5rem;
font-size: ${(props) => props.theme.font.size.md};
gap: 0.75rem;
.button-icon {
width: 1.125rem;
height: 1.125rem;
}
.spinner-icon {
width: 1.125rem;
height: 1.125rem;
}
`
};
const roundedStyles = {
sm: css`
border-radius: ${(props) => props.theme.border.radius.sm};
`,
base: css`
border-radius: ${(props) => props.theme.border.radius.base};
`,
md: css`
border-radius: ${(props) => props.theme.border.radius.md};
`,
lg: css`
border-radius: ${(props) => props.theme.border.radius.lg};
`,
full: css`
border-radius: 9999px;
`
};
const fontWeightStyles = {
regular: 400,
medium: 500
};
// For secondary, use text color for outline/ghost; for others, use bg
const getDisplayColor = (colorConfig, colorName) => {
return colorName === 'secondary' ? colorConfig.text : colorConfig.bg;
};
const getVariantStyles = (variant, color) => {
if (variant === 'filled') {
return css`
background-color: ${(props) => props.theme.button2.color[color].bg};
color: ${(props) => props.theme.button2.color[color].text};
border: 1px solid ${(props) => props.theme.button2.color[color].bg};
&:disabled {
color: ${(props) => props.theme.button2.color[color].text} !important;
}
&:hover:not(:disabled) {
${(props) => {
const bg = props.theme.button2.color[color].bg;
return css`
background-color: ${darken(0.03, bg)};
border-color: ${darken(0.03, bg)};
`;
}}
}
&:active:not(:disabled) {
${(props) => {
const bg = props.theme.button2.color[color].bg;
return css`
background-color: ${darken(0.07, bg)};
`;
}}
}
`;
}
if (variant === 'outline') {
return css`
background-color: transparent;
color: ${(props) => getDisplayColor(props.theme.button2.color[color], color)};
border: 1px solid ${(props) => getDisplayColor(props.theme.button2.color[color], color)};
&:hover:not(:disabled) {
${(props) => {
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
return css`
background-color: ${rgba(displayColor, 0.05)};
`;
}}
}
&:active:not(:disabled) {
${(props) => {
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
return css`
background-color: ${rgba(displayColor, 0.1)};
`;
}}
}
`;
}
if (variant === 'ghost') {
return css`
background-color: transparent;
color: ${(props) => getDisplayColor(props.theme.button2.color[color], color)};
border: 1px solid transparent;
&:hover:not(:disabled) {
${(props) => {
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
return css`
background-color: ${rgba(displayColor, 0.1)};
`;
}}
}
&:active:not(:disabled) {
${(props) => {
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
return css`
background-color: ${rgba(displayColor, 0.15)};
`;
}}
}
`;
}
return css``;
};
const StyledWrapper = styled.div`
display: ${(props) => (props.$fullWidth ? 'block' : 'inline-block')};
width: ${(props) => (props.$fullWidth ? '100%' : 'auto')};
button {
display: inline-flex;
align-items: center;
justify-content: center;
width: ${(props) => (props.$fullWidth ? '100%' : 'auto')};
font-family: inherit;
font-weight: ${(props) => fontWeightStyles[props.$fontWeight] || 400};
line-height: 1;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
white-space: nowrap;
user-select: none;
${(props) => sizeStyles[props.$size]}
${(props) => roundedStyles[props.$rounded]}
${(props) => getVariantStyles(props.$variant, props.$color)}
&:focus-visible {
${(props) => {
const colorConfig = props.theme.button2.color[props.$color];
const focusColor = props.$color === 'secondary' ? colorConfig.text : colorConfig.bg;
return css`
box-shadow: 0 0 0 2px ${rgba(focusColor, 0.4)};
`;
}}
}
&:disabled {
cursor: not-allowed;
pointer-events: none;
opacity: 0.7;
}
.button-content {
display: inline-flex;
align-items: center;
}
.button-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
svg {
width: 100%;
height: 100%;
}
}
.button-spinner {
display: inline-flex;
align-items: center;
justify-content: center;
.spinner-icon {
animation: ${spin} 0.75s linear infinite;
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const Button = ({
children,
size = 'base',
variant = 'filled',
color = 'primary',
disabled = false,
loading = false,
icon,
iconPosition = 'left',
fullWidth = false,
type = 'button',
rounded = 'sm',
fontWeight,
onClick,
onDoubleClick,
className = '',
...rest
}) => {
const handleClick = (e) => {
if (disabled || loading) return;
onClick?.(e);
};
const handleDoubleClick = (e) => {
if (disabled || loading) return;
onDoubleClick?.(e);
};
return (
<StyledWrapper
$size={size}
$variant={variant}
$color={color}
$disabled={disabled}
$loading={loading}
$fullWidth={fullWidth}
$rounded={rounded}
$fontWeight={fontWeight}
$hasIcon={!!icon}
$iconPosition={iconPosition}
className={className}
>
<button
type={type}
disabled={disabled || loading}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
{...rest}
>
{loading && (
<span className="button-spinner">
<svg className="spinner-icon" viewBox="0 0 24 24">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="3"
fill="none"
strokeLinecap="round"
strokeDasharray="31.4 31.4"
/>
</svg>
</span>
)}
{icon && iconPosition === 'left' && !loading && (
<span className="button-icon button-icon-left">{icon}</span>
)}
{children && <span className="button-content">{children}</span>}
{icon && iconPosition === 'right' && !loading && (
<span className="button-icon button-icon-right">{icon}</span>
)}
</button>
</StyledWrapper>
);
};
export default Button;

View File

@@ -0,0 +1,35 @@
const path = require('path');
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
const config = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-webpack5-compiler-babel'
],
framework: {
name: '@storybook/react-webpack5',
options: {}
},
docs: {
autodocs: true
},
webpackFinal: async (config) => {
// Add path aliases to match jsconfig.json
config.resolve.alias = {
...config.resolve.alias,
assets: path.resolve(__dirname, '../src/assets'),
ui: path.resolve(__dirname, '../src/ui'),
components: path.resolve(__dirname, '../src/components'),
hooks: path.resolve(__dirname, '../src/hooks'),
themes: path.resolve(__dirname, '../src/themes'),
api: path.resolve(__dirname, '../src/api'),
pageComponents: path.resolve(__dirname, '../src/pageComponents'),
providers: path.resolve(__dirname, '../src/providers'),
utils: path.resolve(__dirname, '../src/utils')
};
return config;
}
};
module.exports = config;

View File

@@ -0,0 +1,73 @@
import React from 'react';
import { ThemeProvider as SCThemeProvider, createGlobalStyle } from 'styled-components';
import themes from 'themes/index';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
const GlobalStyle = createGlobalStyle`
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
`;
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1e1e1e' }
]
}
},
globalTypes: {
theme: {
description: 'Global theme for components',
toolbar: {
title: 'Theme',
icon: 'paintbrush',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' }
],
dynamicTitle: true
}
}
},
initialGlobals: {
theme: 'light'
},
decorators: [
(Story, context) => {
const themeName = context.globals.theme || 'light';
const theme = themes[themeName];
// Update background and text color based on theme
const isDark = themeName === 'dark';
const backgroundColor = isDark ? '#1e1e1e' : '#ffffff';
const textColor = isDark ? '#d4d4d4' : '#333333';
document.body.style.backgroundColor = backgroundColor;
document.body.style.color = textColor;
return (
<SCThemeProvider theme={theme}>
<GlobalStyle />
<div style={{ padding: '1rem', color: textColor }}>
<Story />
</div>
</SCThemeProvider>
);
}
]
};
export default preview;