mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 01:41:29 +00:00
feat: button storybook
This commit is contained in:
@@ -117,6 +117,18 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'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'],
|
||||
ignores: ['**/*.config.js'],
|
||||
|
||||
2211
package-lock.json
generated
2211
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,11 @@
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"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",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
|
||||
1
packages/bruno-app/.gitignore
vendored
1
packages/bruno-app/.gitignore
vendored
@@ -22,6 +22,7 @@ build
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"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": {
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
@@ -63,6 +65,7 @@
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
|
||||
@@ -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 {
|
||||
font-size: 11px !important;
|
||||
max-width: 200px !important;
|
||||
@@ -123,65 +103,11 @@ const Wrapper = styled.div`
|
||||
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 {
|
||||
padding: 12px 2px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
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;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'providers/ReduxStore/slices/global-environments';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -425,14 +426,14 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
<button type="button" className="submit" onClick={handleSave} data-testid="save-env">
|
||||
<div className="button-container mt-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="submit" size="sm" onClick={handleSave} data-testid="save-env">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" className="submit reset ml-2" onClick={handleReset} data-testid="reset-env">
|
||||
</Button>
|
||||
<Button type="reset" size="sm" color="secondary" variant="ghost" onClick={handleReset} data-testid="reset-env">
|
||||
Reset
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,11 +2,13 @@ const colors = {
|
||||
BRAND: '#d9a342',
|
||||
TEXT: '#d4d4d4',
|
||||
TEXT_MUTED: '#858585',
|
||||
TEXT_LINK: '#569cd6',
|
||||
TEXT_LINK: '#8BB7E0',
|
||||
BG: '#1e1e1e',
|
||||
|
||||
GREEN: '#4ec9b0',
|
||||
YELLOW: '#d9a342',
|
||||
WHITE: '#fff',
|
||||
BLACK: '#000',
|
||||
|
||||
GRAY_1: '#252526',
|
||||
GRAY_2: '#3D3D3D',
|
||||
@@ -343,6 +345,30 @@ const darkTheme = {
|
||||
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: {
|
||||
marginRight: '1.2rem',
|
||||
|
||||
@@ -348,7 +348,30 @@ const lightTheme = {
|
||||
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: {
|
||||
marginRight: '1.2rem',
|
||||
active: {
|
||||
|
||||
430
packages/bruno-app/src/ui/Button/Button.stories.jsx
Normal file
430
packages/bruno-app/src/ui/Button/Button.stories.jsx
Normal 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>
|
||||
)
|
||||
};
|
||||
274
packages/bruno-app/src/ui/Button/StyledWrapper.js
Normal file
274
packages/bruno-app/src/ui/Button/StyledWrapper.js
Normal 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;
|
||||
81
packages/bruno-app/src/ui/Button/index.js
Normal file
81
packages/bruno-app/src/ui/Button/index.js
Normal 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;
|
||||
35
packages/bruno-app/storybook/main.js
Normal file
35
packages/bruno-app/storybook/main.js
Normal 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;
|
||||
73
packages/bruno-app/storybook/preview.jsx
Normal file
73
packages/bruno-app/storybook/preview.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user