Migrate twenty ui to linaria (#18307)

## Migrate twenty-ui from Emotion to Linaria

Completes the migration of all `twenty-ui` components from Emotion
(runtime CSS-in-JS) to Linaria (zero-runtime, CSS extracted at build
time).

- Replaced `@emotion/styled` with `@linaria/react` across ~170 files
- Removed all Emotion dependencies from `twenty-ui`
- Introduced a CSS custom properties-based theme system:
`themeCssVariables` where every leaf is a `var(--t-xxx)` reference,
injected onto `document.documentElement` by
`ThemeCssVariableInjectorEffect`
- No more `theme` prop threading — styled components reference
`themeCssVariables.x.y` directly at build time
- Updated `twenty-front` consumers to remove `theme={theme}` prop
passing

**Before / After:**
```tsx
// Emotion
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(4)};

// Linaria
color: ${themeCssVariables.font.color.primary};
padding: ${themeCssVariables.spacing[4]};
```

### Theme architecture

Two build-time utilities produce the theme system:

- **`buildThemeReferencingRootCssVariables`** — walks the theme object
and builds a nested mirror where every leaf is a `var(--t-xxx)` string
(evaluated at build time by wyw-in-js)
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime
theme and collects flat `[--css-variable-name, value]` pairs, injected
onto `document.documentElement` by `ThemeCssVariableInjectorEffect`

Both share naming conventions (`camelToKebab`, `SPACING_VALUES`,
`formatSpacingKey`) and are unit tested.

### Spacing cleanup

Spacing scale now uses integers 0–32 (generated via loop), with `0.5`
and `1.5` as the only fractional exceptions. All other fractional
spacing usages (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) were replaced with
literal pixel values across ~20 twenty-front files.

### Framer Motion integration

Linaria doesn't support `styled(motion.div)` — wrapping a motion element
with `styled()` causes the component body to be stripped at build time.
Instead, we define the styled component first, then wrap it with
`motion.create()`:

```tsx
const StyledBarBase = styled.div`
  background-color: ${themeCssVariables.font.color.primary};
  height: 100%;
`;

const StyledBar = motion.create(StyledBarBase);
```

### Block interpolations

Linaria doesn't support interpolations that return multiple CSS
declarations (Linaria wraps the entire block in a single `var()`,
producing invalid CSS). These were split into individual property
interpolations:

```tsx
// Emotion — single interpolation returning multiple declarations
border-left: ${({ divider, theme }) => {
  const border = `1px solid ${theme.border.color.light}`;
  return divider ? `border-${divider}: ${border}` : '';
}}

// Linaria — one interpolation per property
border-left: ${({ divider }) =>
  divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
border-right: ${({ divider }) =>
  divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
```

### Dynamic styles via CSS variables

When a component needs to compute styles from multiple props with
complex branching logic (e.g. `Button` combining `variant`, `accent`,
`inverted`, `disabled`, `focus`, `position`), Linaria's prop
interpolations become unwieldy. In those cases we use a
`computeDynamicStyles` function that returns a `CSSProperties` object
injected via `style={}`, referenced from the static CSS with `var()`:

```tsx
const StyledButton = styled.button`
  background: var(--btn-bg);
  border-color: var(--btn-border-color);
  &:hover { background: var(--btn-hover-bg); }
`;

const dynamicStyles = useMemo(() => {
  const s = computeButtonDynamicStyles(variant, accent, ...);
  return { '--btn-bg': s.background, '--btn-hover-bg': s.hoverBackground } as CSSProperties;
}, [variant, accent, ...]);

return <StyledButton style={dynamicStyles} />;
```

### CSS var + unit concatenation

CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Values that need units use `calc()`:

```tsx
// Broken
transition: background ${themeCssVariables.animation.duration.instant}s ease;

// Fixed
transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
This commit is contained in:
Charles Bochet
2026-03-01 15:13:42 +01:00
committed by GitHub
parent 159bb9d70a
commit 1db2a40961
211 changed files with 3682 additions and 2997 deletions
@@ -1,13 +1,15 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { useIsMobile } from '@ui/utilities';
import { getOsShortcutSeparator } from '@ui/utilities/device/getOsShortcutSeparator';
import { type MotionProps, motion } from 'framer-motion';
import React, { useContext, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Pill } from '@ui/components/Pill/Pill';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import {
type ButtonAccent,
type ButtonPosition,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
@@ -19,16 +21,294 @@ export type AnimatedButtonProps = ButtonProps &
soonLabel?: string;
};
type AnimatedButtonDynamicStyles = {
background: string;
borderColor: string;
borderWidthOverride: string;
boxShadow: string;
color: string;
hoverBackground: string;
activeBackground: string;
};
const computeAnimatedButtonDynamicStyles = (
variant: ButtonVariant,
inverted: boolean,
accent: ButtonAccent,
disabled: boolean,
focus: boolean,
position: ButtonPosition,
): AnimatedButtonDynamicStyles => {
const result: AnimatedButtonDynamicStyles = {
background: 'transparent',
borderColor: 'transparent',
borderWidthOverride: '',
boxShadow: 'none',
color: themeCssVariables.font.color.secondary,
hoverBackground: 'transparent',
activeBackground: 'transparent',
};
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
result.background = !inverted
? themeCssVariables.background.secondary
: themeCssVariables.background.primary;
result.borderColor = !inverted
? !disabled && focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.light
: themeCssVariables.background.transparent.light;
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.secondary;
if (!disabled) {
result.hoverBackground = !inverted
? themeCssVariables.background.tertiary
: themeCssVariables.background.secondary;
result.activeBackground = !inverted
? themeCssVariables.background.quaternary
: themeCssVariables.background.tertiary;
} else {
result.hoverBackground = result.background;
result.activeBackground = result.background;
}
break;
case 'blue':
result.background = !inverted
? themeCssVariables.color.blue
: themeCssVariables.background.primary;
result.borderColor = !inverted
? focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.light
: themeCssVariables.background.transparent.light;
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? themeCssVariables.grayScale.gray1
: themeCssVariables.color.blue;
if (!disabled) {
result.hoverBackground = !inverted
? themeCssVariables.color.blue10
: themeCssVariables.background.secondary;
result.activeBackground = !inverted
? themeCssVariables.color.blue12
: themeCssVariables.background.tertiary;
} else {
result.hoverBackground = result.background;
result.activeBackground = result.background;
}
break;
case 'danger':
result.background = !inverted
? themeCssVariables.color.red
: themeCssVariables.background.primary;
result.borderColor = !inverted
? focus
? themeCssVariables.color.red
: themeCssVariables.background.transparent.light
: themeCssVariables.background.transparent.light;
result.borderWidthOverride = '1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.color.red3
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? themeCssVariables.background.primary
: themeCssVariables.color.red;
if (!disabled) {
result.hoverBackground = !inverted
? themeCssVariables.color.red8
: themeCssVariables.background.secondary;
result.activeBackground = !inverted
? themeCssVariables.color.red10
: themeCssVariables.background.tertiary;
} else {
result.hoverBackground = result.background;
result.activeBackground = result.background;
}
break;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
result.background = 'transparent';
result.borderColor = !inverted
? variant === 'secondary'
? !disabled && focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.medium
: focus
? themeCssVariables.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? themeCssVariables.grayScale.gray1
: themeCssVariables.background.transparent.primary
: focus
? themeCssVariables.grayScale.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.inverted;
result.hoverBackground = !inverted
? !disabled
? themeCssVariables.background.transparent.light
: 'transparent'
: themeCssVariables.background.transparent.light;
result.activeBackground = !inverted
? !disabled
? themeCssVariables.background.transparent.light
: 'transparent'
: themeCssVariables.background.transparent.medium;
break;
case 'blue':
result.background = 'transparent';
result.borderColor = !inverted
? variant === 'secondary'
? focus
? themeCssVariables.color.blue
: themeCssVariables.accent.primary
: focus
? themeCssVariables.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? themeCssVariables.grayScale.gray1
: themeCssVariables.background.transparent.primary
: focus
? themeCssVariables.grayScale.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? !disabled
? themeCssVariables.color.blue
: themeCssVariables.accent.accent4060
: themeCssVariables.font.color.inverted;
result.hoverBackground = !inverted
? !disabled
? themeCssVariables.accent.tertiary
: 'transparent'
: themeCssVariables.background.transparent.light;
result.activeBackground = !inverted
? !disabled
? themeCssVariables.accent.secondary
: 'transparent'
: themeCssVariables.background.transparent.medium;
break;
case 'danger':
result.background = 'transparent';
result.borderColor = !inverted
? variant === 'secondary'
? focus
? themeCssVariables.color.red
: themeCssVariables.border.color.danger
: focus
? themeCssVariables.color.red
: 'transparent'
: variant === 'secondary'
? focus || disabled
? themeCssVariables.grayScale.gray1
: themeCssVariables.background.transparent.primary
: focus
? themeCssVariables.grayScale.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.color.red3
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? themeCssVariables.font.color.danger
: themeCssVariables.font.color.inverted;
result.hoverBackground = !inverted
? !disabled
? themeCssVariables.background.danger
: 'transparent'
: themeCssVariables.background.transparent.light;
result.activeBackground = !inverted
? !disabled
? themeCssVariables.background.danger
: 'transparent'
: themeCssVariables.background.transparent.medium;
break;
}
break;
}
if (result.borderWidthOverride !== '' && position !== 'standalone') {
switch (position) {
case 'left':
result.borderWidthOverride = '1px 0px 1px 1px';
break;
case 'middle':
result.borderWidthOverride = '1px 0px 1px 0px';
break;
case 'right':
result.borderWidthOverride = '1px 1px 1px 0px';
break;
}
}
return result;
};
const StyledButton = styled.button<
Pick<
ButtonProps,
| 'fullWidth'
| 'variant'
| 'inverted'
| 'size'
| 'position'
| 'accent'
| 'focus'
| 'justify'
| 'to'
| 'target'
@@ -37,292 +317,44 @@ const StyledButton = styled.button<
>
>`
align-items: center;
${({ theme, variant, inverted, accent, disabled, focus }) => {
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
return css`
background: ${!inverted
? theme.background.secondary
: theme.background.primary};
border-color: ${!inverted
? !disabled && focus
? theme.color.blue
: theme.background.transparent.light
: theme.background.transparent.light};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? !disabled
? theme.font.color.secondary
: theme.font.color.extraLight
: theme.font.color.secondary};
&:hover {
background: ${!inverted
? theme.background.tertiary
: theme.background.secondary};
}
&:active {
background: ${!inverted
? theme.background.quaternary
: theme.background.tertiary};
}
`;
case 'blue':
return css`
background: ${!inverted
? theme.color.blue
: theme.background.primary};
border-color: ${!inverted
? focus
? theme.color.blue
: theme.background.transparent.light
: theme.background.transparent.light};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted ? theme.grayScale.gray1 : theme.color.blue};
${disabled
? ''
: css`
&:hover {
background: ${!inverted
? theme.color.blue10
: theme.background.secondary};
}
&:active {
background: ${!inverted
? theme.color.blue12
: theme.background.tertiary};
}
`}
`;
case 'danger':
return css`
background: ${!inverted
? theme.color.red
: theme.background.primary};
border-color: ${!inverted
? focus
? theme.color.red
: theme.background.transparent.light
: theme.background.transparent.light};
border-width: 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.color.red3
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted ? theme.background.primary : theme.color.red};
${disabled
? ''
: css`
&:hover {
background: ${!inverted
? theme.color.red8
: theme.background.secondary};
}
&:active {
background: ${!inverted
? theme.color.red10
: theme.background.tertiary};
}
`}
`;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
return css`
background: transparent;
border-color: ${!inverted
? variant === 'secondary'
? !disabled && focus
? theme.color.blue
: theme.background.transparent.medium
: focus
? theme.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? theme.grayScale.gray1
: theme.background.transparent.primary
: focus
? theme.grayScale.gray1
: 'transparent'};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? !disabled
? theme.font.color.secondary
: theme.font.color.extraLight
: theme.font.color.inverted};
&:hover {
background: ${!inverted
? !disabled
? theme.background.transparent.light
: 'transparent'
: theme.background.transparent.light};
}
&:active {
background: ${!inverted
? !disabled
? theme.background.transparent.light
: 'transparent'
: theme.background.transparent.medium};
}
`;
case 'blue':
return css`
background: transparent;
border-color: ${!inverted
? variant === 'secondary'
? focus
? theme.color.blue
: theme.accent.primary
: focus
? theme.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? theme.grayScale.gray1
: theme.background.transparent.primary
: focus
? theme.grayScale.gray1
: 'transparent'};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? !disabled
? theme.color.blue
: theme.accent.accent4060
: theme.font.color.inverted};
&:hover {
background: ${!inverted
? !disabled
? theme.accent.tertiary
: 'transparent'
: theme.background.transparent.light};
}
&:active {
background: ${!inverted
? !disabled
? theme.accent.secondary
: 'transparent'
: theme.background.transparent.medium};
}
`;
case 'danger':
return css`
background: transparent;
border-color: ${!inverted
? variant === 'secondary'
? focus
? theme.color.red
: theme.border.color.danger
: focus
? theme.color.red
: 'transparent'
: variant === 'secondary'
? focus || disabled
? theme.grayScale.gray1
: theme.background.transparent.primary
: focus
? theme.grayScale.gray1
: 'transparent'};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.color.red3
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? theme.font.color.danger
: theme.font.color.inverted};
&:hover {
background: ${!inverted
? !disabled
? theme.background.danger
: 'transparent'
: theme.background.transparent.light};
}
&:active {
background: ${!inverted
? !disabled
? theme.background.danger
: 'transparent'
: theme.background.transparent.medium};
}
`;
}
}
}}
background: var(--abtn-bg);
border-color: var(--abtn-border-color);
border-width: var(--abtn-border-width);
box-shadow: var(--abtn-box-shadow);
color: var(--abtn-color);
&:hover {
background: var(--abtn-hover-bg);
}
&:active {
background: var(--abtn-active-bg);
}
text-decoration: none;
border-radius: ${({ position, theme }) => {
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
return themeCssVariables.border.radius.sm;
}
return '';
}};
border-style: solid;
border-width: ${({ variant, position }) => {
switch (variant) {
case 'primary':
case 'secondary':
return position === 'middle' ? '1px 0px' : '1px';
case 'tertiary':
return '0';
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-family: ${themeCssVariables.font.family};
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
font-size: ${themeCssVariables.font.size.md};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: ${({ justify }) => justify};
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)}`;
}};
justify-content: ${({ justify }) => justify ?? ''};
padding: 0 ${themeCssVariables.spacing[2]};
transition: background 0.1s ease;
@@ -343,18 +375,20 @@ const StyledSeparator = styled.div<{
buttonSize: ButtonSize;
accent: ButtonAccent;
}>`
background: ${({ theme, accent }) => {
background: ${({ accent }) => {
switch (accent) {
case 'blue':
return theme.border.color.blue;
return themeCssVariables.border.color.blue;
case 'danger':
return theme.border.color.danger;
return themeCssVariables.border.color.danger;
default:
return theme.font.color.light;
return themeCssVariables.font.color.light;
}
}};
height: ${({ theme, buttonSize }) =>
theme.spacing(buttonSize === 'small' ? 2 : 4)};
height: ${({ buttonSize }) =>
buttonSize === 'small'
? themeCssVariables.spacing[2]
: themeCssVariables.spacing[4]};
margin: 0;
width: 1px;
`;
@@ -363,19 +397,19 @@ const StyledShortcutLabel = styled.div<{
variant: ButtonVariant;
accent: ButtonAccent;
}>`
color: ${({ theme, variant, accent }) => {
color: ${({ variant, accent }) => {
switch (accent) {
case 'blue':
return theme.border.color.blue;
return themeCssVariables.border.color.blue;
case 'danger':
return variant === 'primary'
? theme.border.color.danger
: theme.color.red8;
? themeCssVariables.border.color.danger
: themeCssVariables.color.red8;
default:
return theme.font.color.light;
return themeCssVariables.font.color.light;
}
}};
font-weight: ${({ theme }) => theme.font.weight.medium};
font-weight: ${themeCssVariables.font.weight.medium};
`;
const StyledIconContainer = styled(motion.div)`
@@ -411,8 +445,29 @@ export const AnimatedButton = ({
dataGloballyPreventClickOutside,
soonLabel = 'Soon',
}: AnimatedButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
const isMobile = useIsMobile();
const isDisabled = soon || disabled;
const dynamicStyles = useMemo(() => {
const s = computeAnimatedButtonDynamicStyles(
variant,
inverted,
accent,
isDisabled,
focus,
position,
);
return {
'--abtn-bg': s.background,
'--abtn-border-color': s.borderColor,
'--abtn-border-width': s.borderWidthOverride || undefined,
'--abtn-box-shadow': s.boxShadow,
'--abtn-color': s.color,
'--abtn-hover-bg': s.hoverBackground,
'--abtn-active-bg': s.activeBackground,
} as React.CSSProperties;
}, [variant, inverted, accent, isDisabled, focus, position]);
const ButtonComponent = to ? Link : 'button';
@@ -420,15 +475,12 @@ export const AnimatedButton = ({
<StyledButton
as={ButtonComponent}
fullWidth={fullWidth}
variant={variant}
inverted={inverted}
size={size}
position={position}
disabled={soon || disabled}
focus={focus}
disabled={isDisabled}
justify={justify}
accent={accent}
className={className}
style={dynamicStyles}
onClick={onClick}
to={to}
target={target}
@@ -1,12 +1,12 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import {
type LightIconButtonAccent,
type LightIconButtonSize,
} from '@ui/input/button/components/LightIconButton';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import { motion, type MotionProps } from 'framer-motion';
import { type ComponentProps, type MouseEvent } from 'react';
import { type ComponentProps, type MouseEvent, useContext } from 'react';
export type AnimatedLightIconButtonProps = {
className?: string;
@@ -29,37 +29,38 @@ const StyledButton = styled.button<
background: transparent;
border: none;
border: ${({ disabled, theme, focus }) =>
!disabled && focus ? `1px solid ${theme.color.blue}` : 'none'};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ disabled, theme, focus }) =>
!disabled && focus ? `0 0 0 3px ${theme.color.blue3}` : 'none'};
color: ${({ theme, accent, active, disabled, focus }) => {
border: ${({ disabled, focus }) =>
!disabled && focus ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
border-radius: ${themeCssVariables.border.radius.sm};
box-shadow: ${({ disabled, focus }) =>
!disabled && focus ? `0 0 0 3px ${themeCssVariables.color.blue3}` : 'none'};
color: ${({ accent, active, disabled, focus }) => {
switch (accent) {
case 'secondary':
return active || focus
? theme.color.blue
? themeCssVariables.color.blue
: !disabled
? theme.font.color.secondary
: theme.font.color.extraLight;
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
case 'tertiary':
return active || focus
? theme.color.blue
? themeCssVariables.color.blue
: !disabled
? theme.font.color.tertiary
: theme.font.color.extraLight;
? themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}
return '';
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: center;
padding: ${({ theme }) => theme.spacing(1)};
padding: ${themeCssVariables.spacing[1]};
transition: background 0.1s ease;
white-space: nowrap;
@@ -67,8 +68,10 @@ const StyledButton = styled.button<
min-width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.light : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.light
: 'transparent'};
}
&:focus {
@@ -76,8 +79,10 @@ const StyledButton = styled.button<
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
`;
@@ -102,7 +107,7 @@ export const AnimatedLightIconButton = ({
onClick,
title,
}: AnimatedLightIconButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledButton
@@ -1,14 +1,12 @@
import isPropValid from '@emotion/is-prop-valid';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display/icon/types/IconComponent';
import { ButtonHotkeys } from '@ui/input/button/components/Button/internal/ButtonHotKeys';
import { ButtonIcon } from '@ui/input/button/components/Button/internal/ButtonIcon';
import { ButtonSoon } from '@ui/input/button/components/Button/internal/ButtonSoon';
import { GRAY_SCALE_LIGHT } from '@ui/theme';
import { GRAY_SCALE_LIGHT, themeCssVariables } from '@ui/theme';
import { useIsMobile } from '@ui/utilities';
import { type ClickOutsideAttributes } from '@ui/utilities/types/ClickOutsideAttributes';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { ButtonText } from './internal/ButtonText';
@@ -42,18 +40,295 @@ export type ButtonProps = {
} & Pick<React.ComponentProps<'button'>, 'type'> &
ClickOutsideAttributes;
const StyledButton = styled('button', {
shouldForwardProp: (prop) =>
!['fullWidth'].includes(prop) && isPropValid(prop),
})<
type ButtonDynamicStyles = {
background: string;
borderColor: string;
borderWidthOverride: string;
boxShadow: string;
color: string;
hoverBackground: string;
activeBackground: string;
};
const computeButtonDynamicStyles = (
variant: ButtonVariant,
accent: ButtonAccent,
inverted: boolean,
disabled: boolean,
focus: boolean,
position: ButtonPosition,
): ButtonDynamicStyles => {
const result: ButtonDynamicStyles = {
background: 'transparent',
borderColor: 'transparent',
borderWidthOverride: '',
boxShadow: 'none',
color: themeCssVariables.font.color.secondary,
hoverBackground: 'transparent',
activeBackground: 'transparent',
};
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
result.background = !inverted
? themeCssVariables.background.secondary
: themeCssVariables.background.primary;
result.borderColor = !inverted
? !disabled && focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.light
: themeCssVariables.background.transparent.light;
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.secondary;
if (!disabled) {
result.hoverBackground = !inverted
? themeCssVariables.background.tertiary
: themeCssVariables.background.secondary;
result.activeBackground = !inverted
? themeCssVariables.background.quaternary
: themeCssVariables.background.tertiary;
} else {
result.hoverBackground = result.background;
result.activeBackground = result.background;
}
break;
case 'blue':
result.background = !inverted
? disabled
? themeCssVariables.accent.accent4060
: themeCssVariables.color.blue
: themeCssVariables.background.primary;
result.borderColor = !inverted
? focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.light
: themeCssVariables.background.transparent.light;
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? GRAY_SCALE_LIGHT.gray1
: themeCssVariables.color.blue;
if (!disabled) {
result.hoverBackground = !inverted
? themeCssVariables.color.blue10
: themeCssVariables.background.secondary;
result.activeBackground = !inverted
? themeCssVariables.color.blue12
: themeCssVariables.background.tertiary;
} else {
result.hoverBackground = result.background;
result.activeBackground = result.background;
}
break;
case 'danger':
result.background = !inverted
? themeCssVariables.color.red
: themeCssVariables.background.primary;
result.borderColor = !inverted
? focus
? themeCssVariables.color.red
: themeCssVariables.background.transparent.light
: themeCssVariables.background.transparent.light;
result.borderWidthOverride = '1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.color.red3
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? themeCssVariables.background.primary
: themeCssVariables.color.red;
if (!disabled) {
result.hoverBackground = !inverted
? themeCssVariables.color.red8
: themeCssVariables.background.secondary;
result.activeBackground = !inverted
? themeCssVariables.color.red10
: themeCssVariables.background.tertiary;
} else {
result.hoverBackground = result.background;
result.activeBackground = result.background;
}
break;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
result.borderColor = !inverted
? variant === 'secondary'
? !disabled && focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.medium
: focus
? themeCssVariables.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
: themeCssVariables.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.inverted;
result.hoverBackground = !inverted
? !disabled
? themeCssVariables.background.transparent.light
: 'transparent'
: themeCssVariables.background.transparent.light;
result.activeBackground = !inverted
? !disabled
? themeCssVariables.background.transparent.light
: 'transparent'
: themeCssVariables.background.transparent.medium;
break;
case 'blue':
result.borderColor = !inverted
? variant === 'secondary'
? focus
? themeCssVariables.color.blue
: themeCssVariables.accent.primary
: focus
? themeCssVariables.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
: themeCssVariables.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.accent.tertiary
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? !disabled
? themeCssVariables.color.blue
: themeCssVariables.accent.accent4060
: themeCssVariables.font.color.inverted;
result.hoverBackground = !inverted
? !disabled
? themeCssVariables.accent.tertiary
: 'transparent'
: themeCssVariables.background.transparent.light;
result.activeBackground = !inverted
? !disabled
? themeCssVariables.accent.secondary
: 'transparent'
: themeCssVariables.background.transparent.medium;
break;
case 'danger':
result.borderColor = !inverted
? variant === 'secondary'
? focus
? themeCssVariables.color.red
: themeCssVariables.border.color.danger
: focus
? themeCssVariables.color.red
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
: themeCssVariables.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
!disabled && focus
? `0 0 0 3px ${
!inverted
? themeCssVariables.color.red3
: themeCssVariables.background.transparent.medium
}`
: 'none';
result.color = !inverted
? !disabled
? themeCssVariables.font.color.danger
: themeCssVariables.color.red5
: themeCssVariables.font.color.inverted;
result.hoverBackground = !inverted
? !disabled
? themeCssVariables.background.danger
: 'transparent'
: themeCssVariables.background.transparent.light;
result.activeBackground = !inverted
? !disabled
? themeCssVariables.background.danger
: 'transparent'
: themeCssVariables.background.transparent.medium;
break;
}
break;
}
if (result.borderWidthOverride !== '' && position !== 'standalone') {
switch (position) {
case 'left':
result.borderWidthOverride = '1px 0px 1px 1px';
break;
case 'middle':
result.borderWidthOverride = '1px 0px 1px 0px';
break;
case 'right':
result.borderWidthOverride = '1px 1px 1px 0px';
break;
}
}
return result;
};
const StyledButton = styled.button<
Pick<
ButtonProps,
| 'fullWidth'
| 'variant'
| 'inverted'
| 'size'
| 'position'
| 'accent'
| 'focus'
| 'justify'
| 'to'
@@ -62,300 +337,37 @@ const StyledButton = styled('button', {
> & { hasIcon: boolean }
>`
align-items: center;
${({ theme, variant, inverted, accent, disabled, focus }) => {
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
return css`
background: ${!inverted
? theme.background.secondary
: theme.background.primary};
border-color: ${!inverted
? !disabled && focus
? theme.color.blue
: theme.background.transparent.light
: theme.background.transparent.light};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? !disabled
? theme.font.color.secondary
: theme.font.color.extraLight
: theme.font.color.secondary};
${disabled
? ''
: css`
&:hover {
background: ${!inverted
? theme.background.tertiary
: theme.background.secondary};
}
&:active {
background: ${!inverted
? theme.background.quaternary
: theme.background.tertiary};
}
`}
`;
case 'blue':
return css`
background: ${!inverted
? disabled
? theme.accent.accent4060
: theme.color.blue
: theme.background.primary};
border-color: ${!inverted
? focus
? theme.color.blue
: theme.background.transparent.light
: theme.background.transparent.light};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted ? GRAY_SCALE_LIGHT.gray1 : theme.color.blue};
${disabled
? ''
: css`
&:hover {
background: ${!inverted
? theme.color.blue10
: theme.background.secondary};
}
&:active {
background: ${!inverted
? theme.color.blue12
: theme.background.tertiary};
}
`}
`;
case 'danger':
return css`
background: ${!inverted
? theme.color.red
: theme.background.primary};
border-color: ${!inverted
? focus
? theme.color.red
: theme.background.transparent.light
: theme.background.transparent.light};
border-width: 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.color.red3
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted ? theme.background.primary : theme.color.red};
${disabled
? ''
: css`
&:hover {
background: ${!inverted
? theme.color.red8
: theme.background.secondary};
}
&:active {
background: ${!inverted
? theme.color.red10
: theme.background.tertiary};
}
`}
`;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
return css`
background: transparent;
border-color: ${!inverted
? variant === 'secondary'
? !disabled && focus
? theme.color.blue
: theme.background.transparent.medium
: focus
? theme.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
: theme.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
: 'transparent'};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? !disabled
? theme.font.color.secondary
: theme.font.color.extraLight
: theme.font.color.inverted};
&:hover {
background: ${!inverted
? !disabled
? theme.background.transparent.light
: 'transparent'
: theme.background.transparent.light};
}
&:active {
background: ${!inverted
? !disabled
? theme.background.transparent.light
: 'transparent'
: theme.background.transparent.medium};
}
`;
case 'blue':
return css`
background: transparent;
border-color: ${!inverted
? variant === 'secondary'
? focus
? theme.color.blue
: theme.accent.primary
: focus
? theme.color.blue
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
: theme.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
: 'transparent'};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.accent.tertiary
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? !disabled
? theme.color.blue
: theme.accent.accent4060
: theme.font.color.inverted};
&:hover {
background: ${!inverted
? !disabled
? theme.accent.tertiary
: 'transparent'
: theme.background.transparent.light};
}
&:active {
background: ${!inverted
? !disabled
? theme.accent.secondary
: 'transparent'
: theme.background.transparent.medium};
}
`;
case 'danger':
return css`
background: transparent;
border-color: ${!inverted
? variant === 'secondary'
? focus
? theme.color.red
: theme.border.color.danger
: focus
? theme.color.red
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
: theme.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
: 'transparent'};
border-width: 1px 1px 1px 1px !important;
box-shadow: ${!disabled && focus
? `0 0 0 3px ${
!inverted
? theme.color.red3
: theme.background.transparent.medium
}`
: 'none'};
color: ${!inverted
? !disabled
? theme.font.color.danger
: theme.color.red5
: theme.font.color.inverted};
&:hover {
background: ${!inverted
? !disabled
? theme.background.danger
: 'transparent'
: theme.background.transparent.light};
}
&:active {
background: ${!inverted
? !disabled
? theme.background.danger
: 'transparent'
: theme.background.transparent.medium};
}
`;
}
}
}}
background: var(--btn-bg);
border-color: var(--btn-border-color);
border-width: var(--btn-border-width);
box-shadow: var(--btn-box-shadow);
color: var(--btn-color);
text-decoration: none;
border-radius: ${({ position, theme }) => {
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
return themeCssVariables.border.radius.sm;
}
return '';
}};
border-style: solid;
border-width: ${({ variant, position }) => {
switch (variant) {
case 'primary':
case 'secondary':
return position === 'middle' ? '1px 0px' : '1px';
case 'tertiary':
return '0';
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-family: ${themeCssVariables.font.family};
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
font-size: ${themeCssVariables.font.size.md};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: ${({ justify }) => justify};
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)} 0 ${theme.spacing(2)}`;
}};
justify-content: ${({ justify }) => justify ?? ''};
padding: 0 ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[2]};
box-sizing: border-box;
transition: background 0.1s ease;
@@ -364,67 +376,78 @@ const StyledButton = styled('button', {
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
&:hover {
background: var(--btn-hover-bg);
}
&:active {
background: var(--btn-active-bg);
}
&:focus {
outline: none;
}
`;
const StyledButtonWrapper = styled.div<
Pick<
ButtonProps,
'isLoading' | 'variant' | 'accent' | 'inverted' | 'disabled' | 'fullWidth'
>
Pick<ButtonProps, 'isLoading' | 'fullWidth'>
>`
${({ theme, variant, accent, inverted, disabled }) => css`
--tw-button-color: ${(() => {
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
return !inverted
? !disabled
? theme.font.color.secondary
: theme.font.color.extraLight
: theme.font.color.secondary;
case 'blue':
return !inverted ? GRAY_SCALE_LIGHT.gray1 : theme.color.blue;
case 'danger':
return !inverted ? theme.background.primary : theme.color.red;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
return !inverted
? !disabled
? theme.font.color.secondary
: theme.font.color.extraLight
: theme.font.color.inverted;
case 'blue':
return !inverted
? !disabled
? theme.color.blue
: theme.accent.accent4060
: theme.font.color.inverted;
case 'danger':
return !inverted
? theme.font.color.danger
: theme.font.color.inverted;
}
break;
}
return theme.font.color.secondary; // Valeur par défaut
})()};
`}
max-width: ${({ isLoading, theme }) =>
isLoading ? `calc(100% - ${theme.spacing(8)})` : 'none'};
max-width: ${({ isLoading }) =>
isLoading ? `calc(100% - ${themeCssVariables.spacing[8]})` : 'none'};
position: relative;
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
`;
const computeButtonWrapperColor = (
variant: ButtonVariant,
accent: ButtonAccent,
inverted: boolean,
disabled: boolean,
): string => {
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
return !inverted
? !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.secondary;
case 'blue':
return !inverted
? GRAY_SCALE_LIGHT.gray1
: themeCssVariables.color.blue;
case 'danger':
return !inverted
? themeCssVariables.background.primary
: themeCssVariables.color.red;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
return !inverted
? !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.inverted;
case 'blue':
return !inverted
? !disabled
? themeCssVariables.color.blue
: themeCssVariables.accent.accent4060
: themeCssVariables.font.color.inverted;
case 'danger':
return !inverted
? themeCssVariables.font.color.danger
: themeCssVariables.font.color.inverted;
}
break;
}
return themeCssVariables.font.color.secondary;
};
export const Button = ({
className,
Icon,
@@ -453,25 +476,47 @@ export const Button = ({
const isMobile = useIsMobile();
const [isFocused, setIsFocused] = useState(propFocus);
const isDisabled = soon || disabled;
const dynamicStyles = useMemo(() => {
const s = computeButtonDynamicStyles(
variant,
accent,
inverted,
isDisabled,
isFocused,
position,
);
return {
'--btn-bg': s.background,
'--btn-border-color': s.borderColor,
'--btn-border-width': s.borderWidthOverride || undefined,
'--btn-box-shadow': s.boxShadow,
'--btn-color': s.color,
'--btn-hover-bg': s.hoverBackground,
'--btn-active-bg': s.activeBackground,
'--tw-button-color': computeButtonWrapperColor(
variant,
accent,
inverted,
isDisabled,
),
} as React.CSSProperties;
}, [variant, accent, inverted, isDisabled, isFocused, position]);
return (
<StyledButtonWrapper
isLoading={!!isLoading}
variant={variant}
accent={accent}
inverted={inverted}
disabled={soon || disabled}
fullWidth={fullWidth}
style={dynamicStyles}
>
<StyledButton
fullWidth={fullWidth}
variant={variant}
inverted={inverted}
position={position}
disabled={soon || disabled}
disabled={isDisabled}
hasIcon={!!Icon}
focus={isFocused}
justify={justify}
accent={accent}
className={className}
onClick={onClick}
to={to}
@@ -486,6 +531,7 @@ export const Button = ({
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
size={size}
style={dynamicStyles}
>
{(isLoading || Icon) && (
<ButtonIcon Icon={Icon} isLoading={!!isLoading} />
@@ -1,27 +1,31 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import {
type ButtonAccent,
type ButtonSize,
type ButtonVariant,
} from '@ui/input';
import { themeCssVariables } from '@ui/theme';
import { getOsShortcutSeparator } from '@ui/utilities';
const StyledSeparator = styled.div<{
buttonSize: ButtonSize;
accent: ButtonAccent;
}>`
background: ${({ theme, accent }) => {
background: ${({ accent }) => {
switch (accent) {
case 'blue':
return theme.buttons.secondaryTextColor;
return themeCssVariables.buttons.secondaryTextColor;
case 'danger':
return theme.border.color.danger;
return themeCssVariables.border.color.danger;
default:
return theme.font.color.light;
return themeCssVariables.font.color.light;
}
}};
height: ${({ theme, buttonSize }) =>
theme.spacing(buttonSize === 'small' ? 2 : 4)};
height: ${({ buttonSize }) =>
buttonSize === 'small'
? themeCssVariables.spacing[2]
: themeCssVariables.spacing[4]};
margin: 0;
width: 1px;
`;
@@ -30,19 +34,19 @@ const StyledShortcutLabel = styled.div<{
variant: ButtonVariant;
accent: ButtonAccent;
}>`
color: ${({ theme, variant, accent }) => {
color: ${({ variant, accent }) => {
switch (accent) {
case 'blue':
return theme.buttons.secondaryTextColor;
return themeCssVariables.buttons.secondaryTextColor;
case 'danger':
return variant === 'primary'
? theme.border.color.danger
: theme.color.red8;
? themeCssVariables.border.color.danger
: themeCssVariables.color.red8;
default:
return theme.font.color.light;
return themeCssVariables.font.color.light;
}
}};
font-weight: ${({ theme }) => theme.font.weight.medium};
font-weight: ${themeCssVariables.font.weight.medium};
`;
export const ButtonHotkeys = ({
@@ -55,11 +59,13 @@ export const ButtonHotkeys = ({
accent: ButtonAccent;
variant: ButtonVariant;
hotkeys: string[];
}) => (
<>
<StyledSeparator buttonSize={size} accent={accent} />
<StyledShortcutLabel variant={variant} accent={accent}>
{hotkeys.join(getOsShortcutSeparator())}
</StyledShortcutLabel>
</>
);
}) => {
return (
<>
<StyledSeparator buttonSize={size} accent={accent} />
<StyledShortcutLabel variant={variant} accent={accent}>
{hotkeys.join(getOsShortcutSeparator())}
</StyledShortcutLabel>
</>
);
};
@@ -1,15 +1,16 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { Loader } from '@ui/feedback';
import { baseTransitionTiming } from '@ui/input/button/components/Button/constant';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import { useContext } from 'react';
const StyledIcon = styled.div<{
isLoading: boolean;
}>`
align-items: center;
display: flex;
height: calc(100% - ${({ theme }) => theme.spacing(4)});
height: calc(100% - ${themeCssVariables.spacing[4]});
color: var(--tw-button-color);
opacity: ${({ isLoading }) => (isLoading ? 0 : 1)};
@@ -26,13 +27,13 @@ const StyledIconWrapper = styled.div`
`;
const StyledLoader = styled.div`
left: ${({ theme }) => theme.spacing(2)};
left: ${themeCssVariables.spacing[2]};
opacity: 1;
position: absolute;
transition: opacity ${baseTransitionTiming / 2}ms ease;
transition-delay: ${baseTransitionTiming / 2}ms;
width: ${({ theme }) => theme.spacing(6)};
width: ${themeCssVariables.spacing[6]};
`;
export const ButtonIcon = ({
@@ -42,7 +43,7 @@ export const ButtonIcon = ({
Icon?: IconComponent;
isLoading?: boolean;
}) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledIconWrapper>
{isLoading && (
@@ -1,4 +1,4 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { Pill } from '@ui/components';
const StyledSoonPill = styled(Pill)`
@@ -1,10 +1,14 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { baseTransitionTiming } from '@ui/input/button/components/Button/constant';
import { themeCssVariables } from '@ui/theme';
const StyledEllipsis = styled.div<{ isLoading?: boolean }>`
right: 0;
clip-path: ${({ theme, isLoading }) =>
isLoading ? `inset(0 0 0 0)` : `inset(0 0 0 ${theme.spacing(6)})`};
clip-path: ${({ isLoading }) =>
isLoading
? `inset(0 0 0 0)`
: `inset(0 0 0 ${themeCssVariables.spacing[6]})`};
overflow: hidden;
position: absolute;
@@ -19,17 +23,20 @@ const StyledTextWrapper = styled.div`
position: relative;
`;
const StyledText = styled.div<{ isLoading?: boolean; hasIcon: boolean }>`
clip-path: ${({ isLoading, theme, hasIcon }) =>
const StyledText = styled.div<{
isLoading?: boolean;
hasIcon: boolean;
}>`
clip-path: ${({ isLoading, hasIcon }) =>
isLoading
? ` inset(0 ${!hasIcon ? theme.spacing(12) : theme.spacing(6)} 0 0)`
? ` inset(0 ${!hasIcon ? themeCssVariables.spacing[12] : themeCssVariables.spacing[6]} 0 0)`
: ' inset(0 0 0 0)'};
overflow: hidden;
transform: ${({ theme, isLoading, hasIcon }) =>
transform: ${({ isLoading, hasIcon }) =>
isLoading
? `translateX(${!hasIcon ? theme.spacing(7) : theme.spacing(3)})`
? `translateX(${!hasIcon ? themeCssVariables.spacing[7] : themeCssVariables.spacing[3]})`
: 'none'};
transition:
@@ -49,11 +56,13 @@ export const ButtonText = ({
isLoading?: boolean;
hasIcon: boolean;
title?: string;
}) => (
<StyledTextWrapper>
<StyledText isLoading={isLoading} hasIcon={hasIcon}>
{title}
</StyledText>
<StyledEllipsis isLoading={isLoading}>...</StyledEllipsis>
</StyledTextWrapper>
);
}) => {
return (
<StyledTextWrapper>
<StyledText isLoading={isLoading} hasIcon={hasIcon}>
{title}
</StyledText>
<StyledEllipsis isLoading={isLoading}>...</StyledEllipsis>
</StyledTextWrapper>
);
};
@@ -1,11 +1,12 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import React, { type ReactNode } from 'react';
import { type ButtonPosition, type ButtonProps } from './Button/Button';
import { themeCssVariables } from '@ui/theme';
import { isDefined } from 'twenty-shared/utils';
const StyledButtonGroupContainer = styled.div`
border-radius: ${({ theme }) => theme.border.radius.md};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
`;
@@ -22,36 +23,38 @@ export const ButtonGroup = ({
variant,
size,
accent,
}: ButtonGroupProps) => (
<StyledButtonGroupContainer className={className}>
{React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return null;
}: ButtonGroupProps) => {
return (
<StyledButtonGroupContainer className={className}>
{React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return null;
let position: ButtonPosition;
let position: ButtonPosition;
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
const additionalProps: any = { position, variant, accent, size };
const additionalProps: any = { position, variant, accent, size };
if (isDefined(variant)) {
additionalProps.variant = variant;
}
if (isDefined(variant)) {
additionalProps.variant = variant;
}
if (isDefined(accent)) {
additionalProps.variant = variant;
}
if (isDefined(accent)) {
additionalProps.accent = accent;
}
if (isDefined(size)) {
additionalProps.size = size;
}
if (isDefined(size)) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledButtonGroupContainer>
);
return React.cloneElement(child, additionalProps);
})}
</StyledButtonGroupContainer>
);
};
@@ -1,6 +1,6 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { ColorSample, type ColorSampleProps } from '@ui/display';
import { themeCssVariables } from '@ui/theme';
import {
LightIconButton,
type LightIconButtonProps,
@@ -11,30 +11,36 @@ type ColorPickerButtonProps = Pick<ColorSampleProps, 'colorName'> &
isSelected?: boolean;
};
const StyledButton = styled(LightIconButton)<{
const StyledButtonWrapper = styled.div<{
isSelected?: boolean;
}>`
${({ isSelected, theme }) =>
isSelected
? css`
background-color: ${theme.background.transparent.medium};
button {
${({ isSelected }) =>
isSelected
? `background-color: ${themeCssVariables.background.transparent.medium};`
: ''}
}
&:hover {
background-color: ${theme.background.transparent.medium};
}
`
: ''}
button:hover {
${({ isSelected }) =>
isSelected
? `background-color: ${themeCssVariables.background.transparent.medium};`
: ''}
}
`;
export const ColorPickerButton = ({
colorName,
isSelected,
onClick,
}: ColorPickerButtonProps) => (
<StyledButton
size="medium"
isSelected={isSelected}
Icon={() => <ColorSample colorName={colorName} />}
onClick={onClick}
/>
);
}: ColorPickerButtonProps) => {
return (
<StyledButtonWrapper isSelected={isSelected}>
<LightIconButton
size="medium"
Icon={() => <ColorSample colorName={colorName} />}
onClick={onClick}
/>
</StyledButtonWrapper>
);
};
@@ -1,7 +1,7 @@
import isPropValid from '@emotion/is-prop-valid';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import { useContext } from 'react';
import { Link } from 'react-router-dom';
export type FloatingButtonSize = 'small' | 'medium';
@@ -20,11 +20,7 @@ export type FloatingButtonProps = {
to?: string;
};
const StyledButton = styled('button', {
shouldForwardProp: (prop) =>
!['applyBlur', 'applyShadow', 'focus', 'position', 'size'].includes(prop) &&
isPropValid(prop),
})<
const StyledButton = styled.button<
Pick<
FloatingButtonProps,
| 'size'
@@ -38,62 +34,65 @@ const StyledButton = styled('button', {
>`
align-items: center;
backdrop-filter: ${({ applyBlur }) => (applyBlur ? 'blur(20px)' : 'none')};
background: ${({ theme }) => theme.background.primary};
background: ${themeCssVariables.background.primary};
border: ${({ focus, theme }) =>
focus ? `1px solid ${theme.color.blue}` : 'none'};
border-radius: ${({ position, theme }) => {
border: ${({ focus }) =>
focus ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
return themeCssVariables.border.radius.sm;
}
return '';
}};
box-shadow: ${({ theme, applyShadow, focus }) =>
box-shadow: ${({ applyShadow, focus }) =>
applyShadow
? `0px 2px 4px 0px ${
theme.background.transparent.light
}, 0px 0px 4px 0px ${theme.background.transparent.medium}${
focus ? `,0 0 0 3px ${theme.color.blue3}` : ''
themeCssVariables.background.transparent.light
}, 0px 0px 4px 0px ${themeCssVariables.background.transparent.medium}${
focus ? `,0 0 0 3px ${themeCssVariables.color.blue3}` : ''
}`
: focus
? `0 0 0 3px ${theme.color.blue3}`
? `0 0 0 3px ${themeCssVariables.color.blue3}`
: 'none'};
color: ${({ theme, disabled, focus }) => {
color: ${({ disabled, focus }) => {
return !disabled
? focus
? theme.color.blue
: theme.font.color.secondary
: theme.font.color.extraLight;
? themeCssVariables.color.blue
: themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)}`;
}};
padding: 0 ${themeCssVariables.spacing[2]};
transition: background 0.1s ease;
white-space: nowrap;
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.lighter : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.lighter
: 'transparent'};
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
&:focus {
@@ -114,7 +113,7 @@ export const FloatingButton = ({
focus = false,
to,
}: FloatingButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledButton
disabled={disabled}
@@ -1,17 +1,19 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import React from 'react';
import {
type FloatingButtonPosition,
type FloatingButtonProps,
} from './FloatingButton';
import { themeCssVariables } from '@ui/theme';
import { isDefined } from 'twenty-shared/utils';
const StyledFloatingButtonGroupContainer = styled.div`
backdrop-filter: blur(20px);
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) =>
`0px 2px 4px 0px ${theme.background.transparent.light}, 0px 0px 4px 0px ${theme.background.transparent.medium}`};
border-radius: ${themeCssVariables.border.radius.md};
box-shadow:
0px 2px 4px 0px ${themeCssVariables.background.transparent.light},
0px 0px 4px 0px ${themeCssVariables.background.transparent.medium};
display: inline-flex;
`;
@@ -24,31 +26,33 @@ export const FloatingButtonGroup = ({
children,
size,
className,
}: FloatingButtonGroupProps) => (
<StyledFloatingButtonGroupContainer className={className}>
{React.Children.map(children, (child, index) => {
let position: FloatingButtonPosition;
}: FloatingButtonGroupProps) => {
return (
<StyledFloatingButtonGroupContainer className={className}>
{React.Children.map(children, (child, index) => {
let position: FloatingButtonPosition;
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
const additionalProps: any = {
position,
size,
applyShadow: false,
applyBlur: false,
};
const additionalProps: any = {
position,
size,
applyShadow: false,
applyBlur: false,
};
if (isDefined(size)) {
additionalProps.size = size;
}
if (isDefined(size)) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledFloatingButtonGroupContainer>
);
return React.cloneElement(child, additionalProps);
})}
</StyledFloatingButtonGroupContainer>
);
};
@@ -1,7 +1,7 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import React from 'react';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import React, { useContext } from 'react';
export type FloatingIconButtonSize = 'small' | 'medium';
export type FloatingIconButtonPosition =
@@ -22,92 +22,81 @@ export type FloatingIconButtonProps = {
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
isActive?: boolean;
};
const shouldForwardProp = (prop: string) =>
![
'applyBlur',
'applyShadow',
'isActive',
'focus',
'position',
'size',
].includes(prop);
const StyledButton = styled('button', { shouldForwardProp })<
const StyledButton = styled.button<
Pick<
FloatingIconButtonProps,
'size' | 'position' | 'applyShadow' | 'applyBlur' | 'focus' | 'isActive'
>
>`
align-items: center;
backdrop-filter: ${({ theme, applyBlur }) =>
applyBlur ? theme.blur.medium : 'none'};
background: ${({ theme, isActive }) =>
isActive ? theme.background.transparent.medium : theme.background.primary};
border: ${({ focus, theme }) =>
backdrop-filter: ${({ applyBlur }) =>
applyBlur ? themeCssVariables.blur.medium : 'none'};
background: ${({ isActive }) =>
isActive
? themeCssVariables.background.transparent.medium
: themeCssVariables.background.primary};
border: ${({ focus }) =>
focus
? `1px solid ${theme.color.blue}`
: `1px solid ${theme.border.color.strong}`};
border-radius: ${({ position, theme }) => {
? `1px solid ${themeCssVariables.color.blue}`
: `1px solid ${themeCssVariables.border.color.strong}`};
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
return themeCssVariables.border.radius.sm;
}
return '';
}};
box-shadow: ${({ theme, applyShadow, focus }) =>
box-shadow: ${({ applyShadow, focus }) =>
applyShadow
? theme.boxShadow.light
? themeCssVariables.boxShadow.light
: focus
? `0 0 0 3px ${theme.color.blue3}`
? `0 0 0 3px ${themeCssVariables.color.blue3}`
: 'none'};
box-sizing: border-box;
color: ${({ theme, disabled, focus }) => {
color: ${({ disabled, focus }) => {
return !disabled
? focus
? theme.color.blue
: theme.font.color.tertiary
: theme.font.color.extraLight;
? themeCssVariables.color.blue
: themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
justify-content: center;
padding: 0;
position: relative;
transition: background ${({ theme }) => theme.animation.duration.instant}s
ease;
transition: background
calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
white-space: nowrap;
${({ position, size }) => {
const sizeInPx =
(size === 'small' ? 24 : 32) - (position === 'standalone' ? 0 : 4);
height: ${({ position, size }) =>
(size === 'small' ? 24 : 32) - (position === 'standalone' ? 0 : 4)}px;
width: ${({ position, size }) =>
(size === 'small' ? 24 : 32) - (position === 'standalone' ? 0 : 4)}px;
return `
height: ${sizeInPx}px;
width: ${sizeInPx}px;
`;
}}
${({ theme, disabled }) =>
!disabled &&
css`
&:hover {
background: ${theme.background.transparent.lighter};
}
`}
&:hover {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.lighter
: 'transparent'};
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
&:focus {
@@ -127,7 +116,7 @@ export const FloatingIconButton = ({
onClick,
isActive,
}: FloatingIconButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledButton
disabled={disabled}
@@ -1,4 +1,4 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { type MouseEvent } from 'react';
@@ -7,13 +7,15 @@ import {
type FloatingIconButtonPosition,
type FloatingIconButtonProps,
} from './FloatingIconButton';
import { themeCssVariables } from '@ui/theme';
const StyledFloatingIconButtonGroupContainer = styled.div`
backdrop-filter: blur(20px);
background-color: ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) =>
`0px 2px 4px 0px ${theme.background.transparent.light}, 0px 0px 4px 0px ${theme.background.transparent.medium}`};
background-color: ${themeCssVariables.background.primary};
border-radius: ${themeCssVariables.border.radius.sm};
box-shadow:
0px 2px 4px 0px ${themeCssVariables.background.transparent.light},
0px 0px 4px 0px ${themeCssVariables.background.transparent.medium};
display: inline-flex;
gap: 2px;
padding: 2px;
@@ -34,30 +36,32 @@ export const FloatingIconButtonGroup = ({
iconButtons,
size,
className,
}: FloatingIconButtonGroupProps) => (
<StyledFloatingIconButtonGroupContainer className={className}>
{iconButtons.map(({ Icon, onClick, isActive }, index) => {
const position: FloatingIconButtonPosition =
iconButtons.length === 1
? 'standalone'
: index === 0
? 'left'
: index === iconButtons.length - 1
? 'right'
: 'middle';
}: FloatingIconButtonGroupProps) => {
return (
<StyledFloatingIconButtonGroupContainer className={className}>
{iconButtons.map(({ Icon, onClick, isActive }, index) => {
const position: FloatingIconButtonPosition =
iconButtons.length === 1
? 'standalone'
: index === 0
? 'left'
: index === iconButtons.length - 1
? 'right'
: 'middle';
return (
<FloatingIconButton
key={`floating-icon-button-${index}`}
applyBlur={false}
applyShadow={false}
Icon={Icon}
onClick={onClick}
position={position}
size={size}
isActive={isActive}
/>
);
})}
</StyledFloatingIconButtonGroupContainer>
);
return (
<FloatingIconButton
key={`floating-icon-button-${index}`}
applyBlur={false}
applyShadow={false}
Icon={Icon}
onClick={onClick}
position={position}
size={size}
isActive={isActive}
/>
);
})}
</StyledFloatingIconButtonGroupContainer>
);
};
@@ -1,8 +1,7 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { GRAY_SCALE_LIGHT } from '@ui/theme';
import React from 'react';
import { GRAY_SCALE_LIGHT, ThemeContext, themeCssVariables } from '@ui/theme';
import React, { useContext, useMemo } from 'react';
export type IconButtonSize = 'medium' | 'small';
export type IconButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
@@ -24,6 +23,192 @@ export type IconButtonProps = {
to?: string;
};
type IconButtonDynamicStyles = {
background: string;
borderColor: string;
borderWidthOverride?: string;
boxShadow: string;
color: string;
opacity: number;
hoverBackground: string;
activeBackground: string;
};
const computeIconButtonDynamicStyles = (
variant: IconButtonVariant,
accent: IconButtonAccent,
disabled: boolean,
focus: boolean,
): IconButtonDynamicStyles => {
const focusOverride = !disabled && focus;
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
return {
background: themeCssVariables.background.secondary,
borderColor: focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.light,
borderWidthOverride: focusOverride ? '1px 1px' : undefined,
boxShadow: focusOverride
? `0 0 0 3px ${themeCssVariables.accent.tertiary}`
: 'none',
color: !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight,
opacity: 1,
hoverBackground: !disabled
? themeCssVariables.background.tertiary
: themeCssVariables.background.secondary,
activeBackground: !disabled
? themeCssVariables.background.quaternary
: themeCssVariables.background.secondary,
};
case 'blue':
return {
background: themeCssVariables.color.blue,
borderColor: !disabled
? focus
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.light
: 'transparent',
borderWidthOverride: focusOverride ? '1px 1px' : undefined,
boxShadow: focusOverride
? `0 0 0 3px ${themeCssVariables.accent.tertiary}`
: 'none',
color: GRAY_SCALE_LIGHT.gray1,
opacity: disabled ? 0.24 : 1,
hoverBackground: disabled
? themeCssVariables.color.blue
: themeCssVariables.color.blue10,
activeBackground: disabled
? themeCssVariables.color.blue
: themeCssVariables.color.blue12,
};
case 'danger':
return {
background: themeCssVariables.color.red,
borderColor: !disabled
? focus
? themeCssVariables.color.red
: themeCssVariables.background.transparent.light
: 'transparent',
borderWidthOverride: focusOverride ? '1px 1px' : undefined,
boxShadow: focusOverride
? `0 0 0 3px ${themeCssVariables.color.red3}`
: 'none',
color: GRAY_SCALE_LIGHT.gray1,
opacity: disabled ? 0.24 : 1,
hoverBackground: disabled
? themeCssVariables.color.red
: themeCssVariables.color.red10,
activeBackground: disabled
? themeCssVariables.color.red
: themeCssVariables.color.red10,
};
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
return {
background: focus
? themeCssVariables.background.transparent.primary
: 'transparent',
borderColor:
variant === 'secondary'
? focusOverride
? themeCssVariables.color.blue
: themeCssVariables.background.transparent.medium
: focus
? themeCssVariables.color.blue
: 'transparent',
borderWidthOverride: focusOverride ? '1px 1px' : undefined,
boxShadow: focusOverride
? `0 0 0 3px ${themeCssVariables.accent.tertiary}`
: 'none',
color: disabled
? themeCssVariables.font.color.extraLight
: variant === 'secondary'
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.tertiary,
opacity: 1,
hoverBackground: !disabled
? themeCssVariables.background.transparent.light
: 'transparent',
activeBackground: !disabled
? themeCssVariables.background.transparent.light
: 'transparent',
};
case 'blue':
return {
background: focus
? themeCssVariables.background.transparent.primary
: 'transparent',
borderColor:
variant === 'secondary'
? !disabled
? themeCssVariables.color.blue
: themeCssVariables.color.blue5
: focus
? themeCssVariables.color.blue
: 'transparent',
borderWidthOverride: focusOverride ? '1px 1px' : undefined,
boxShadow: focusOverride
? `0 0 0 3px ${themeCssVariables.accent.tertiary}`
: 'none',
color: !disabled
? themeCssVariables.color.blue
: themeCssVariables.accent.accent4060,
opacity: 1,
hoverBackground: !disabled
? themeCssVariables.accent.tertiary
: 'transparent',
activeBackground: !disabled
? themeCssVariables.accent.secondary
: 'transparent',
};
case 'danger':
return {
background: 'transparent',
borderColor:
variant === 'secondary'
? themeCssVariables.border.color.danger
: focus
? themeCssVariables.color.red
: 'transparent',
borderWidthOverride: focusOverride ? '1px 1px' : undefined,
boxShadow: focusOverride
? `0 0 0 3px ${themeCssVariables.color.red3}`
: 'none',
color: !disabled
? themeCssVariables.font.color.danger
: themeCssVariables.color.red5,
opacity: 1,
hoverBackground: !disabled
? themeCssVariables.background.danger
: 'transparent',
activeBackground: !disabled
? themeCssVariables.background.danger
: 'transparent',
};
}
}
return {
background: 'transparent',
borderColor: 'transparent',
boxShadow: 'none',
color: themeCssVariables.font.color.secondary,
opacity: 1,
hoverBackground: 'transparent',
activeBackground: 'transparent',
};
};
const StyledButton = styled.button<
Pick<
IconButtonProps,
@@ -31,206 +216,52 @@ const StyledButton = styled.button<
>
>`
align-items: center;
${({ theme, variant, accent, disabled, focus }) => {
switch (variant) {
case 'primary':
switch (accent) {
case 'default':
return css`
background: ${theme.background.secondary};
border-color: ${focus
? theme.color.blue
: theme.background.transparent.light};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'};
color: ${!disabled
? theme.font.color.secondary
: theme.font.color.extraLight};
&:hover {
background: ${!disabled
? theme.background.tertiary
: theme.background.secondary};
}
&:active {
background: ${!disabled
? theme.background.quaternary
: theme.background.secondary};
}
`;
case 'blue':
return css`
background: ${theme.color.blue};
border-color: ${!disabled
? focus
? theme.color.blue
: theme.background.transparent.light
: 'transparent'};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'};
color: ${GRAY_SCALE_LIGHT.gray1};
opacity: ${disabled ? 0.24 : 1};
background: var(--ibtn-bg);
border-color: var(--ibtn-border-color);
box-shadow: var(--ibtn-box-shadow);
color: var(--ibtn-color);
opacity: var(--ibtn-opacity, 1);
&:hover {
background: var(--ibtn-hover-bg);
}
&:active {
background: var(--ibtn-active-bg);
}
${disabled
? ''
: css`
&:hover {
background: ${theme.color.blue10};
}
&:active {
background: ${theme.color.blue12};
}
`}
`;
case 'danger':
return css`
background: ${theme.color.red};
border-color: ${!disabled
? focus
? theme.color.red
: theme.background.transparent.light
: 'transparent'};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${!disabled && focus
? `0 0 0 3px ${theme.color.red3}`
: 'none'};
color: ${GRAY_SCALE_LIGHT.gray1};
opacity: ${disabled ? 0.24 : 1};
${disabled
? ''
: css`
&:hover,
&:active {
background: ${theme.color.red10};
}
`}
`;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
return css`
background: ${focus
? theme.background.transparent.primary
: 'transparent'};
border-color: ${variant === 'secondary'
? !disabled && focus
? theme.color.blue
: theme.background.transparent.medium
: focus
? theme.color.blue
: 'transparent'};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'};
color: ${disabled
? theme.font.color.extraLight
: variant === 'secondary'
? theme.font.color.secondary
: theme.font.color.tertiary};
&:hover {
background: ${!disabled
? theme.background.transparent.light
: 'transparent'};
}
&:active {
background: ${!disabled
? theme.background.transparent.light
: 'transparent'};
}
`;
case 'blue':
return css`
background: ${focus
? theme.background.transparent.primary
: 'transparent'};
border-color: ${variant === 'secondary'
? !disabled
? theme.color.blue
: theme.color.blue5
: focus
? theme.color.blue
: 'transparent'};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'};
color: ${!disabled ? theme.color.blue : theme.accent.accent4060};
&:hover {
background: ${!disabled
? theme.accent.tertiary
: 'transparent'};
}
&:active {
background: ${!disabled
? theme.accent.secondary
: 'transparent'};
}
`;
case 'danger':
return css`
background: transparent;
border-color: ${variant === 'secondary'
? theme.border.color.danger
: focus
? theme.color.red
: 'transparent'};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${!disabled && focus
? `0 0 0 3px ${theme.color.red3}`
: 'none'};
color: ${!disabled ? theme.font.color.danger : theme.color.red5};
&:hover {
background: ${!disabled
? theme.background.danger
: 'transparent'};
}
&:active {
background: ${!disabled
? theme.background.danger
: 'transparent'};
}
`;
}
}
}}
border-radius: ${({ position, theme }) => {
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
return themeCssVariables.border.radius.sm;
}
return '';
}};
border-style: solid;
border-width: ${({ variant, position }) => {
switch (variant) {
case 'primary':
case 'secondary':
return position === 'middle' ? '1px 0px' : '1px';
case 'tertiary':
return '0';
}
}};
border-width: var(
--ibtn-border-width,
${({ variant, position }) => {
switch (variant) {
case 'primary':
case 'secondary':
return position === 'middle' ? '1px 0px' : '1px';
case 'tertiary':
return '0';
}
return '';
}}
);
box-sizing: border-box;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-family: ${themeCssVariables.font.family};
font-weight: 500;
gap: ${({ theme }) => theme.spacing(1)};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: center;
padding: 0;
@@ -259,7 +290,26 @@ export const IconButton = ({
onClick,
to,
}: IconButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
const dynamicStyles = useMemo(() => {
const styles = computeIconButtonDynamicStyles(
variant,
accent,
disabled,
focus,
);
return {
'--ibtn-bg': styles.background,
'--ibtn-border-color': styles.borderColor,
'--ibtn-border-width': styles.borderWidthOverride || undefined,
'--ibtn-box-shadow': styles.boxShadow,
'--ibtn-color': styles.color,
'--ibtn-opacity': styles.opacity,
'--ibtn-hover-bg': styles.hoverBackground,
'--ibtn-active-bg': styles.activeBackground,
} as React.CSSProperties;
}, [variant, accent, disabled, focus]);
return (
<StyledButton
data-testid={dataTestId}
@@ -273,6 +323,7 @@ export const IconButton = ({
onClick={onClick}
aria-label={ariaLabel}
to={to}
style={dynamicStyles}
>
{Icon && <Icon size={theme.icon.size.md} />}
</StyledButton>
@@ -1,8 +1,9 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { type MouseEvent } from 'react';
import { InsideButton } from '@ui/input/button/components/InsideButton';
import { themeCssVariables } from '@ui/theme';
export type IconButtonGroupProps = {
disabled?: boolean;
@@ -18,16 +19,16 @@ const StyledIconButtonGroupContainer = styled.div<
>`
display: inline-flex;
align-items: flex-start;
background-color: ${({ disabled, theme }) =>
disabled ? 'inherit' : theme.background.transparent.lighter};
border-radius: ${({ theme }) => theme.border.radius.sm};
border: 1px solid ${({ theme }) => theme.border.color.strong};
background-color: ${({ disabled }) =>
disabled ? 'inherit' : themeCssVariables.background.transparent.lighter};
border-radius: ${themeCssVariables.border.radius.sm};
border: 1px solid ${themeCssVariables.border.color.strong};
gap: 2px;
padding: 2px;
backdrop-filter: blur(20px);
&:hover {
box-shadow: ${({ theme }) => theme.boxShadow.light};
box-shadow: ${themeCssVariables.boxShadow.light};
}
`;
@@ -35,17 +36,19 @@ export const IconButtonGroup = ({
iconButtons,
disabled,
className,
}: IconButtonGroupProps) => (
<StyledIconButtonGroupContainer className={className} disabled={disabled}>
{iconButtons.map(({ Icon, onClick }, index) => {
return (
<InsideButton
key={index}
Icon={Icon}
onClick={onClick}
disabled={disabled}
/>
);
})}
</StyledIconButtonGroupContainer>
);
}: IconButtonGroupProps) => {
return (
<StyledIconButtonGroupContainer className={className} disabled={disabled}>
{iconButtons.map(({ Icon, onClick }, index) => {
return (
<InsideButton
key={index}
Icon={Icon}
onClick={onClick}
disabled={disabled}
/>
);
})}
</StyledIconButtonGroupContainer>
);
};
@@ -1,7 +1,7 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import styled from '@emotion/styled';
import React from 'react';
import { useTheme } from '@emotion/react';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import React, { useContext } from 'react';
export type InsideButtonProps = {
className?: string;
@@ -14,8 +14,8 @@ const StyledButton = styled.button`
align-items: center;
border: none;
background-color: transparent;
border-radius: ${({ theme }) => theme.border.radius.xs};
color: ${({ theme }) => theme.font.color.tertiary};
border-radius: ${themeCssVariables.border.radius.xs};
color: ${themeCssVariables.font.color.tertiary};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
@@ -27,7 +27,7 @@ const StyledButton = styled.button`
transition: background-color 0.1s ease;
&:hover {
background-color: ${({ theme }) => theme.background.transparent.light};
background-color: ${themeCssVariables.background.transparent.light};
}
`;
@@ -37,7 +37,7 @@ export const InsideButton = ({
onClick,
disabled = false,
}: InsideButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledButton className={className} onClick={onClick} disabled={disabled}>
@@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { type MouseEvent } from 'react';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import { type MouseEvent, useContext } from 'react';
export type LightButtonAccent = 'secondary' | 'tertiary';
@@ -22,47 +22,48 @@ const StyledButton = styled.button<
>`
align-items: center;
background: transparent;
border: ${({ theme, focus }) =>
focus ? `1px solid ${theme.color.blue}` : 'none'};
border: ${({ focus }) =>
focus ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme, focus }) =>
focus ? `0 0 0 3px ${theme.color.blue3}` : 'none'};
color: ${({ theme, accent, active, disabled, focus }) => {
border-radius: ${themeCssVariables.border.radius.sm};
box-shadow: ${({ focus }) =>
focus ? `0 0 0 3px ${themeCssVariables.color.blue3}` : 'none'};
color: ${({ accent, active, disabled, focus }) => {
switch (accent) {
case 'secondary':
return active || focus
? theme.color.blue
? themeCssVariables.color.blue
: !disabled
? theme.font.color.secondary
: theme.font.color.extraLight;
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
case 'tertiary':
return active || focus
? theme.color.blue
? themeCssVariables.color.blue
: !disabled
? theme.font.color.tertiary
: theme.font.color.extraLight;
? themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}
return '';
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: 24px;
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)}`;
}};
padding: 0 ${themeCssVariables.spacing[2]};
transition: background 0.1s ease;
white-space: nowrap;
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.light : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.light
: 'transparent'};
}
&:focus {
@@ -70,8 +71,10 @@ const StyledButton = styled.button<
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
`;
@@ -86,7 +89,7 @@ export const LightButton = ({
type = 'button',
onClick,
}: LightButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledButton
@@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { type ComponentProps, type MouseEvent } from 'react';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import { type ComponentProps, type MouseEvent, useContext } from 'react';
export type LightIconButtonAccent = 'secondary' | 'tertiary';
export type LightIconButtonSize = 'small' | 'medium';
@@ -26,49 +26,58 @@ const StyledButton = styled.button<
background: transparent;
border: none;
border: ${({ disabled, theme, focus }) =>
!disabled && focus ? `1px solid ${theme.color.blue}` : 'none'};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ disabled, theme, focus }) =>
!disabled && focus ? `0 0 0 3px ${theme.color.blue3}` : 'none'};
color: ${({ theme, accent, active, disabled, focus }) => {
border: ${({ disabled, focus }) =>
!disabled && focus ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
border-radius: ${themeCssVariables.border.radius.sm};
box-shadow: ${({ disabled, focus }) =>
!disabled && focus ? `0 0 0 3px ${themeCssVariables.color.blue3}` : 'none'};
color: ${({ accent, active, disabled, focus }) => {
switch (accent) {
case 'secondary':
return active || focus
? theme.color.blue
? themeCssVariables.color.blue
: !disabled
? theme.font.color.secondary
: theme.font.color.extraLight;
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
case 'tertiary':
return active || focus
? theme.color.blue
? themeCssVariables.color.blue
: !disabled
? theme.font.color.tertiary
: theme.font.color.extraLight;
? themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}
return '';
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size, theme }) =>
size === 'small' ? theme.spacing(6) : theme.spacing(8)};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) =>
size === 'small'
? themeCssVariables.spacing[6]
: themeCssVariables.spacing[8]};
justify-content: center;
padding: ${({ theme }) => theme.spacing(1)};
padding: ${themeCssVariables.spacing[1]};
transition: background 0.1s ease;
white-space: nowrap;
width: ${({ size, theme }) =>
size === 'small' ? theme.spacing(6) : theme.spacing(8)};
min-width: ${({ size, theme }) =>
size === 'small' ? theme.spacing(6) : theme.spacing(8)};
width: ${({ size }) =>
size === 'small'
? themeCssVariables.spacing[6]
: themeCssVariables.spacing[8]};
min-width: ${({ size }) =>
size === 'small'
? themeCssVariables.spacing[6]
: themeCssVariables.spacing[8]};
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.light : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.light
: 'transparent'};
}
&:focus {
@@ -76,8 +85,10 @@ const StyledButton = styled.button<
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
`;
@@ -94,7 +105,7 @@ export const LightIconButton = ({
onClick,
title,
}: LightIconButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledButton
@@ -1,4 +1,4 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import {
type FunctionComponent,
@@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import React, { type FunctionComponent } from 'react';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import React, { type FunctionComponent, useContext } from 'react';
export type MainButtonVariant = 'primary' | 'secondary';
@@ -17,89 +17,76 @@ const StyledButton = styled.button<
Pick<Props, 'fullWidth' | 'width' | 'variant'>
>`
align-items: center;
background: ${({ theme, variant, disabled }) => {
background: ${({ variant, disabled }) => {
if (disabled === true) {
return theme.background.secondary;
return themeCssVariables.background.secondary;
}
switch (variant) {
case 'primary':
return theme.background.primaryInverted;
return themeCssVariables.background.primaryInverted;
case 'secondary':
return theme.background.primary;
return themeCssVariables.background.primary;
default:
return theme.background.primary;
return themeCssVariables.background.primary;
}
}};
border: 1px solid;
border-color: ${({ theme, disabled, variant }) => {
border-color: ${({ disabled, variant }) => {
if (disabled === true) {
return theme.background.transparent.lighter;
return themeCssVariables.background.transparent.lighter;
}
switch (variant) {
case 'primary':
return theme.background.transparent.strong;
return themeCssVariables.background.transparent.strong;
case 'secondary':
return theme.border.color.medium;
return themeCssVariables.border.color.medium;
default:
return theme.background.primary;
return themeCssVariables.background.primary;
}
}};
border-radius: ${({ theme }) => theme.border.radius.md};
${({ theme, disabled }) => {
border-radius: ${themeCssVariables.border.radius.md};
box-shadow: ${({ disabled }) =>
disabled ? 'none' : themeCssVariables.boxShadow.light};
color: ${({ variant, disabled }) => {
if (disabled === true) {
return '';
}
return `box-shadow: ${theme.boxShadow.light};`;
}}
color: ${({ theme, variant, disabled }) => {
if (disabled === true) {
return theme.font.color.light;
return themeCssVariables.font.color.light;
}
switch (variant) {
case 'primary':
return theme.font.color.inverted;
return themeCssVariables.font.color.inverted;
case 'secondary':
return theme.font.color.primary;
return themeCssVariables.font.color.primary;
default:
return theme.font.color.primary;
return themeCssVariables.font.color.primary;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
gap: ${({ theme }) => theme.spacing(2)};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.semiBold};
gap: ${themeCssVariables.spacing[2]};
justify-content: center;
outline: none;
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
max-height: ${({ theme }) => theme.spacing(8)};
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
max-height: ${themeCssVariables.spacing[8]};
width: ${({ fullWidth, width }) =>
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
${({ theme, variant, disabled }) => {
switch (variant) {
case 'secondary':
return `
&:hover {
background: ${theme.background.tertiary};
}
`;
default:
return `
&:hover {
background: ${
!disabled
? theme.background.primaryInvertedHover
: theme.background.secondary
};};
}
`;
}
}};
&:hover {
background: ${({ variant, disabled }) => {
switch (variant) {
case 'secondary':
return themeCssVariables.background.tertiary;
default:
return !disabled
? themeCssVariables.background.primaryInvertedHover
: themeCssVariables.background.secondary;
}
}};
}
`;
type MainButtonProps = Props & {
@@ -117,7 +104,7 @@ export const MainButton = ({
disabled,
className,
}: MainButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledButton
className={className}
@@ -1,15 +1,18 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import { useContext } from 'react';
export type RoundedIconButtonSize = 'small' | 'medium';
const StyledIconButton = styled.button<{ size: RoundedIconButtonSize }>`
const StyledIconButton = styled.button<{
size: RoundedIconButtonSize;
}>`
align-items: center;
background: ${({ theme }) => theme.color.blue};
background: ${themeCssVariables.color.blue};
border: none;
border-radius: 50%;
color: ${({ theme }) => theme.font.color.inverted};
color: ${themeCssVariables.font.color.inverted};
cursor: pointer;
display: flex;
height: ${({ size }) => (size === 'small' ? '20px' : '24px')};
@@ -21,8 +24,8 @@ const StyledIconButton = styled.button<{ size: RoundedIconButtonSize }>`
background 0.1s ease-in-out;
&:disabled {
background: ${({ theme }) => theme.background.quaternary};
color: ${({ theme }) => theme.font.color.tertiary};
background: ${themeCssVariables.background.quaternary};
color: ${themeCssVariables.font.color.tertiary};
cursor: default;
}
width: ${({ size }) => (size === 'small' ? '20px' : '24px')};
@@ -40,7 +43,7 @@ export const RoundedIconButton = ({
className,
size = 'small',
}: RoundedIconButtonProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledIconButton
@@ -1,27 +1,25 @@
import isPropValid from '@emotion/is-prop-valid';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { themeCssVariables } from '@ui/theme';
export const StyledTabButton = styled('button', {
shouldForwardProp: (prop) => isPropValid(prop) && prop !== 'active',
})<{
export const StyledTabButton = styled.button<{
active?: boolean;
disabled?: boolean;
to?: string;
}>`
all: unset;
align-items: center;
color: ${({ theme, active, disabled }) =>
color: ${({ active, disabled }) =>
active
? theme.font.color.primary
? themeCssVariables.font.color.primary
: disabled
? theme.font.color.light
: theme.font.color.secondary};
? themeCssVariables.font.color.light
: themeCssVariables.font.color.secondary};
cursor: pointer;
background-color: transparent;
border: none;
font-family: inherit;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
gap: ${themeCssVariables.spacing[1]};
justify-content: center;
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
text-decoration: none;
@@ -33,8 +31,8 @@ export const StyledTabButton = styled('button', {
left: 0;
right: 0;
height: 1px;
background-color: ${({ theme, active }) =>
active ? theme.border.color.inverted : 'transparent'};
background-color: ${({ active }) =>
active ? themeCssVariables.border.color.inverted : 'transparent'};
z-index: 1;
}
`;
@@ -44,16 +42,16 @@ export const StyledTabContainer = styled.div<{
disabled?: boolean;
}>`
align-items: center;
color: ${({ theme, active, disabled }) =>
color: ${({ active, disabled }) =>
active
? theme.font.color.primary
? themeCssVariables.font.color.primary
: disabled
? theme.font.color.light
: theme.font.color.secondary};
? themeCssVariables.font.color.light
: themeCssVariables.font.color.secondary};
cursor: pointer;
background-color: transparent;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
gap: ${themeCssVariables.spacing[1]};
justify-content: center;
text-decoration: none;
position: relative;
@@ -65,8 +63,8 @@ export const StyledTabContainer = styled.div<{
left: 0;
right: 0;
height: 1px;
background-color: ${({ theme, active }) =>
active ? theme.border.color.inverted : 'transparent'};
background-color: ${({ active }) =>
active ? themeCssVariables.border.color.inverted : 'transparent'};
z-index: 1;
}
`;
@@ -75,19 +73,19 @@ export const StyledTabHover = styled.span<{
contentSize?: 'sm' | 'md';
}>`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme, contentSize }) =>
gap: ${themeCssVariables.spacing[1]};
padding: ${({ contentSize }) =>
contentSize === 'sm'
? `${theme.spacing(1)} ${theme.spacing(2)}`
: `${theme.spacing(2)} ${theme.spacing(2)}`};
font-weight: ${({ theme }) => theme.font.weight.medium};
? `${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]}`
: `${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]}`};
font-weight: ${themeCssVariables.font.weight.medium};
width: 100%;
white-space: nowrap;
border-radius: ${({ theme }) => theme.border.radius.sm};
border-radius: ${themeCssVariables.border.radius.sm};
&:hover {
background: ${({ theme }) => theme.background.tertiary};
background: ${themeCssVariables.background.tertiary};
}
&:active {
background: ${({ theme }) => theme.background.quaternary};
background: ${themeCssVariables.background.quaternary};
}
`;
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
/* eslint-disable react/jsx-props-no-spreading */
import { styled } from '@linaria/react';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import {
IconCheckbox,
@@ -15,11 +16,12 @@ import {
ComponentWithRouterDecorator,
JotaiRootDecorator,
} from '@ui/testing';
import { type ReactNode } from 'react';
import { themeCssVariables } from '@ui/theme';
// Mimic the TabList container styling for proper positioning
const StyledTabContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
gap: ${themeCssVariables.spacing[1]};
height: 40px;
user-select: none;
position: relative;
@@ -32,10 +34,14 @@ const StyledTabContainer = styled.div`
left: 0;
right: 0;
height: 1px;
background-color: ${({ theme }) => theme.border.color.light};
background-color: ${themeCssVariables.border.color.light};
}
`;
const TabContainer = ({ children }: { children?: ReactNode }) => {
return <StyledTabContainer>{children}</StyledTabContainer>;
};
const meta: Meta<typeof TabButton> = {
title: 'UI/Input/Button/TabButton',
component: TabButton,
@@ -67,22 +73,9 @@ export const Default: Story = {
LeftIcon: IconSettings,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -93,22 +86,9 @@ export const Active: Story = {
active: true,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -119,22 +99,9 @@ export const Disabled: Story = {
disabled: true,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -144,22 +111,9 @@ export const WithLogo: Story = {
logo: 'https://picsum.photos/192/192',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -170,22 +124,9 @@ export const WithStringPill: Story = {
pill: '12',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -196,22 +137,9 @@ export const WithBothIcons: Story = {
RightIcon: IconChevronDown,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -222,22 +150,9 @@ export const AsLink: Story = {
to: '/profile',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -248,22 +163,9 @@ export const SmallContent: Story = {
contentSize: 'sm',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -274,22 +176,9 @@ export const MediumContent: Story = {
contentSize: 'md',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
@@ -305,22 +194,9 @@ export const Catalog: CatalogStory<Story, typeof TabButton> = {
to: { control: false },
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },