Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/components/PillTag/PillTag.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { colors, typography, radii } from '../../globals';

export const PILL_TAG_VARIANTS = ['primary', 'secondary'] as const;
export type PillTagVariant = (typeof PILL_TAG_VARIANTS)[number];
export const VALIDATION_MESSAGES = {
EMPTY_LABEL: 'PillTag: label prop is required and cannot be empty',
INVALID_VARIANT: (variant: string) =>
`PillTag: invalid variant "${variant}". Using "primary" as fallback.`,
} as const;
export const pillTagTokens = {
dimensions: {
primary: {
width: 251.51,
height: 85.1,
borderRadius: radii.pillTag.primary,
fontSize: 28.37,
padding: {
vertical: 2.5,
horizontal: 4,
},
},
secondary: {
width: 227.17,
height: 76.86,
borderRadius: radii.pillTag.secondary,
fontSize: 25.62,
padding: {
vertical: 2.2,
horizontal: 3.5,
},
},
},
colors: {
background: colors.background.paper,
gradientStart: colors.primary.main,
gradientEnd: colors.primary.light,
},
shadows: {
primary: {
default: `0px 18.91px 66.19px 0px ${colors.shadows.black15}`,
hover: `0px 20.91px 71.19px 0px ${colors.shadows.black20}`,
active: `0px 16.91px 56.19px 0px ${colors.shadows.black18}`,
},
secondary: {
default: `0px 17.08px 59.78px 0px ${colors.shadows.black15}`,
hover: `0px 19.08px 64.78px 0px ${colors.shadows.black20}`,
active: `0px 15.08px 49.78px 0px ${colors.shadows.black18}`,
},
},
typography: {
fontFamily: typography.fontFamily.primary,
fontWeight: typography.fontWeight.medium,
lineHeight: typography.lineHeight.none,
letterSpacing: typography.letterSpacing.normal,
},
states: {
hover: {
translateY: -2,
},
active: {
scale: 0.98,
},
disabled: {
opacity: 0.5,
},
},
transitions: {
duration: 200,
easing: 'ease-in-out',
},
} as const;
187 changes: 187 additions & 0 deletions src/components/PillTag/PillTag.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PillTag } from './PillTag';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { Box, CssBaseline } from '@mui/material';
import { colors, typography } from '../../globals';

const theme = createTheme({
palette: {
primary: {
main: colors.primary.main,
light: colors.primary.light,
dark: colors.primary.dark,
contrastText: colors.primary.contrastText,
},
secondary: {
main: colors.secondary.main,
light: colors.secondary.light,
dark: colors.secondary.dark,
contrastText: colors.secondary.contrastText,
},
background: {
default: colors.background.default,
paper: colors.background.paper,
},
},
typography: {
fontFamily: typography.fontFamily.primary,
},
});

const meta: Meta<typeof PillTag> = {
title: 'Components/PillTag',
component: PillTag,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: `
PillTag Component usando Design Tokens globales (src/globals/).

**Características:**
- Tokens centralizados en src/globals/
- Pixel-perfect según Figma
- WCAG AA compliant
- 10 tests unitarios
`,
},
},
},
argTypes: {
label: {
control: 'text',
description: 'Texto a mostrar en el tag',
},
variant: {
control: 'radio',
options: ['primary', 'secondary'],
description: 'Variante visual',
},
clickable: {
control: 'boolean',
},
disabled: {
control: 'boolean',
},
},
};

export default meta;
type Story = StoryObj<typeof PillTag>;

export const Primary: Story = {
render: (args) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ p: 4 }}>
<PillTag {...args} />
</Box>
</ThemeProvider>
),
args: {
label: 'Courses',
variant: 'primary',
},
};

export const Secondary: Story = {
render: (args) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ p: 4 }}>
<PillTag {...args} />
</Box>
</ThemeProvider>
),
args: {
label: 'Test',
variant: 'secondary',
},
};

export const Clickable: Story = {
render: (args) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ p: 4 }}>
<PillTag {...args} />
</Box>
</ThemeProvider>
),
args: {
label: 'Click me',
variant: 'primary',
clickable: true,
onClick: () => console.log('Clicked!'),
},
};

export const Deleteable: Story = {
render: (args) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ p: 4 }}>
<PillTag {...args} />
</Box>
</ThemeProvider>
),
args: {
label: 'Delete me',
variant: 'secondary',
onDelete: () => console.log('Deleted!'),
},
};

export const Disabled: Story = {
render: (args) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ p: 4 }}>
<PillTag {...args} />
</Box>
</ThemeProvider>
),
args: {
label: 'Disabled',
disabled: true,
},
};

export const Multiple: Story = {
render: () => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ p: 4, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<PillTag label="React" variant="primary" />
<PillTag label="TypeScript" variant="secondary" />
<PillTag label="Astro" variant="primary" />
<PillTag label="MUI" variant="secondary" />
</Box>
</ThemeProvider>
),
};
export const Comparison: Story = {
decorators: [
(Story) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
padding: 4,
display: 'flex',
flexDirection: 'column',
gap: 5,
}}
>
<Story />
</Box>
</ThemeProvider>
),
],
render: () => (
<>
<PillTag label="Courses" variant="primary" />
<PillTag label="Test" variant="secondary" />
</>
),
};
79 changes: 79 additions & 0 deletions src/components/PillTag/PillTag.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { styled } from '@mui/material/styles';
import Chip from '@mui/material/Chip';
import type { ChipProps } from '@mui/material/Chip';
import type { PillTagVariant } from './PillTag.constants';
import { pillTagTokens } from './PillTag.constants';

interface PillTagStyledProps extends ChipProps {
pillVariant?: PillTagVariant;
}
export const PillTagStyled = styled(Chip, {
shouldForwardProp: (prop) => prop !== 'pillVariant',
})<PillTagStyledProps>(({ theme, pillVariant = 'primary' }) => {
const dimensions = pillTagTokens.dimensions[pillVariant];
const shadows = pillTagTokens.shadows[pillVariant];

return {
fontFamily: pillTagTokens.typography.fontFamily,
fontWeight: pillTagTokens.typography.fontWeight,
fontStyle: 'normal',
lineHeight: pillTagTokens.typography.lineHeight,
fontSize: `${dimensions.fontSize}px`,
letterSpacing: pillTagTokens.typography.letterSpacing,
textAlign: 'center',
textTransform: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
border: 'none',
boxSizing: 'border-box',
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
backgroundColor: pillTagTokens.colors.background,
borderRadius:
pillVariant === 'primary'
? `${dimensions.borderRadius} 0px ${dimensions.borderRadius} ${dimensions.borderRadius}`
: `${dimensions.borderRadius} ${dimensions.borderRadius} ${dimensions.borderRadius} 0px`,
boxShadow: shadows.default,
padding: theme.spacing(
dimensions.padding.vertical,
dimensions.padding.horizontal
),
transition: theme.transitions.create(
['box-shadow', 'transform'],
{
duration: pillTagTokens.transitions.duration,
easing: pillTagTokens.transitions.easing,
}
),
'& .MuiChip-label': {
display: 'block',
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
padding: 0,
background: `linear-gradient(180deg, ${pillTagTokens.colors.gradientStart} 0%, ${pillTagTokens.colors.gradientEnd} 100%)`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
},
'&:hover': {
boxShadow: shadows.hover,
transform: `translateY(${pillTagTokens.states.hover.translateY}px)`,
},
'&:active': {
boxShadow: shadows.active,
transform: `scale(${pillTagTokens.states.active.scale})`,
},
'&.Mui-disabled': {
opacity: pillTagTokens.states.disabled.opacity,
pointerEvents: 'none',
boxShadow: 'none',
},
'&:focus-visible': {
outline: `2px solid ${pillTagTokens.colors.gradientStart}`,
outlineOffset: '2px',
},
};
});
Loading