Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)

## Description

Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name
**`twenty-ui`** (v0.1.0, publishable) and renames the old package to
**`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports →
`twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates
twenty-front's `Toggle` to the new package (first consumer) as a
drop-in.

## Next steps
- Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` +
`.yarnrc.yml`), then tag `ui/v0.1.0` to publish.
- Continue migrating components from `twenty-ui-deprecated` →
`twenty-ui`.
This commit is contained in:
Raphaël Bosi
2026-06-08 18:12:28 +02:00
committed by GitHub
parent a91e737e69
commit c596a5e342
2320 changed files with 6313 additions and 4856 deletions
@@ -1,518 +0,0 @@
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-constants';
import { GRAY_SCALE_LIGHT } from '@ui/theme/constants/GrayScaleLight';
import {
type ButtonAccent,
type ButtonPosition,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
} from './Button/Button';
export type AnimatedButtonProps = ButtonProps &
Pick<MotionProps, 'animate' | 'transition'> & {
animatedSvg: React.ReactNode;
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
? 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.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
? 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.background = 'transparent';
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.background = 'transparent';
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
? 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'
| 'size'
| 'position'
| 'justify'
| 'to'
| 'target'
| 'dataClickOutsideId'
| 'dataGloballyPreventClickOutside'
>
>`
align-items: center;
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 }) => {
switch (position) {
case 'left':
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return themeCssVariables.border.radius.sm;
}
return '';
}};
border-style: solid;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${themeCssVariables.font.family};
font-weight: 500;
font-size: ${themeCssVariables.font.size.md};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: ${({ justify }) => justify ?? ''};
padding: 0 ${themeCssVariables.spacing[2]};
transition: background 0.1s ease;
white-space: nowrap;
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
&:focus {
outline: none;
}
`;
const StyledSoonPillContainer = styled.span`
display: flex;
margin-left: auto;
`;
const StyledSeparator = styled.div<{
buttonSize: ButtonSize;
accent: ButtonAccent;
}>`
background: ${({ accent }) => {
switch (accent) {
case 'blue':
return themeCssVariables.border.color.blue;
case 'danger':
return themeCssVariables.border.color.danger;
default:
return themeCssVariables.font.color.light;
}
}};
height: ${({ buttonSize }) =>
buttonSize === 'small'
? themeCssVariables.spacing[2]
: themeCssVariables.spacing[4]};
margin: 0;
width: 1px;
`;
const StyledShortcutLabel = styled.div<{
variant: ButtonVariant;
accent: ButtonAccent;
}>`
color: ${({ variant, accent }) => {
switch (accent) {
case 'blue':
return themeCssVariables.border.color.blue;
case 'danger':
return variant === 'primary'
? themeCssVariables.border.color.danger
: themeCssVariables.color.red8;
default:
return themeCssVariables.font.color.light;
}
}};
font-weight: ${themeCssVariables.font.weight.medium};
`;
const StyledMotion = styled(motion.div)`
display: flex;
`;
export const AnimatedButton = ({
className,
Icon,
animatedSvg,
title,
fullWidth = false,
variant = 'primary',
inverted = false,
size = 'medium',
accent = 'default',
position = 'standalone',
soon = false,
disabled = false,
justify = 'flex-start',
focus = false,
onClick,
to,
target,
dataTestId,
hotkeys,
ariaLabel,
animate,
transition,
dataClickOutsideId,
dataGloballyPreventClickOutside,
soonLabel = 'Soon',
}: AnimatedButtonProps) => {
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';
return (
<StyledButton
as={ButtonComponent}
fullWidth={fullWidth}
size={size}
position={position}
disabled={isDisabled}
justify={justify}
className={className}
style={dynamicStyles}
onClick={onClick}
to={to}
target={target}
data-testid={dataTestId}
aria-label={ariaLabel}
data-click-outside-id={dataClickOutsideId}
data-globally-prevent-click-outside={dataGloballyPreventClickOutside}
>
{Icon && (
<StyledMotion animate={animate} transition={transition}>
<Icon size={theme.icon.size.sm} />
</StyledMotion>
)}
{animatedSvg && (
<StyledMotion animate={animate} transition={transition}>
{animatedSvg}
</StyledMotion>
)}
{title}
{hotkeys && !isMobile && (
<>
<StyledSeparator buttonSize={size} accent={accent} />
<StyledShortcutLabel variant={variant} accent={accent}>
{hotkeys.join(getOsShortcutSeparator())}
</StyledShortcutLabel>
</>
)}
{soon && (
<StyledSoonPillContainer>
<Pill label={soonLabel} />
</StyledSoonPillContainer>
)}
</StyledButton>
);
};
@@ -1,134 +0,0 @@
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-constants';
import { motion, type MotionProps } from 'framer-motion';
import { type ComponentProps, type MouseEvent, useContext } from 'react';
export type AnimatedLightIconButtonProps = {
className?: string;
testId?: string;
Icon?: IconComponent;
title?: string;
size?: LightIconButtonSize;
accent?: LightIconButtonAccent;
active?: boolean;
disabled?: boolean;
focus?: boolean;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
} & Pick<ComponentProps<'button'>, 'aria-label' | 'title'> &
Pick<MotionProps, 'animate' | 'transition'>;
const StyledButton = styled.button<
Pick<AnimatedLightIconButtonProps, 'accent' | 'active' | 'size' | 'focus'>
>`
align-items: center;
background: transparent;
border: none;
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
? themeCssVariables.color.blue
: !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
case 'tertiary':
return active || focus
? themeCssVariables.color.blue
: !disabled
? themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}
return '';
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
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: ${themeCssVariables.spacing[1]};
transition: background 0.1s ease;
white-space: nowrap;
width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
min-width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
&:hover {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.light
: 'transparent'};
}
&:focus {
outline: none;
}
&:active {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
`;
const StyledIconContainer = styled(motion.div)`
display: flex;
align-items: center;
justify-content: center;
`;
export const AnimatedLightIconButton = ({
'aria-label': ariaLabel,
className,
testId,
animate,
transition,
Icon,
active = false,
size = 'small',
accent = 'secondary',
disabled = false,
focus = false,
onClick,
title,
}: AnimatedLightIconButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
data-testid={testId}
aria-label={ariaLabel}
onClick={onClick}
disabled={disabled}
focus={focus && !disabled}
accent={accent}
className={className}
size={size}
active={active}
title={title}
>
<StyledIconContainer animate={animate} transition={transition}>
{Icon && (
<Icon
size={size === 'medium' ? theme.icon.size.md : theme.icon.size.sm}
/>
)}
</StyledIconContainer>
</StyledButton>
);
};
@@ -1,560 +0,0 @@
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 { themeCssVariables } from '@ui/theme-constants';
import { GRAY_SCALE_LIGHT } from '@ui/theme/constants/GrayScaleLight';
import { useIsMobile } from '@ui/utilities';
import { type ClickOutsideAttributes } from '@ui/utilities/types/ClickOutsideAttributes';
import React, { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { ButtonText } from './internal/ButtonText';
export type ButtonSize = 'medium' | 'small';
export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type ButtonAccent = 'default' | 'blue' | 'danger';
export type ButtonProps = {
id?: string;
className?: string;
Icon?: IconComponent;
title?: string;
fullWidth?: boolean;
variant?: ButtonVariant;
inverted?: boolean;
size?: ButtonSize;
position?: ButtonPosition;
accent?: ButtonAccent;
soon?: boolean;
justify?: 'center' | 'flex-start' | 'flex-end';
disabled?: boolean;
focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
to?: string;
target?: string;
dataTestId?: string;
hotkeys?: string[];
ariaLabel?: string;
isLoading?: boolean;
} & Pick<React.ComponentProps<'button'>, 'type'> &
ClickOutsideAttributes;
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'
| 'size'
| 'position'
| 'focus'
| 'justify'
| 'to'
| 'target'
| 'isLoading'
> & { hasIcon: boolean }
>`
align-items: center;
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 }) => {
switch (position) {
case 'left':
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return themeCssVariables.border.radius.sm;
}
return '';
}};
border-style: solid;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${themeCssVariables.font.family};
font-weight: 500;
font-size: ${themeCssVariables.font.size.md};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: ${({ justify }) => justify ?? ''};
padding: 0 ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[2]};
box-sizing: border-box;
transition: background 0.1s ease;
white-space: nowrap;
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' | 'fullWidth'>
>`
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
? !disabled
? themeCssVariables.font.color.danger
: themeCssVariables.color.red5
: themeCssVariables.font.color.inverted;
}
break;
}
return themeCssVariables.font.color.secondary;
};
export const Button = ({
className,
Icon,
title,
id,
fullWidth = false,
variant = 'primary',
inverted = false,
size = 'medium',
accent = 'default',
position = 'standalone',
soon = false,
disabled = false,
justify = 'flex-start',
focus: propFocus = false,
onClick,
to,
target,
dataTestId,
dataClickOutsideId,
dataGloballyPreventClickOutside,
hotkeys,
ariaLabel,
type,
isLoading = false,
}: ButtonProps) => {
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}
fullWidth={fullWidth}
style={dynamicStyles}
>
<StyledButton
id={id}
fullWidth={fullWidth}
position={position}
disabled={isDisabled}
hasIcon={!!Icon}
focus={isFocused}
justify={justify}
className={className}
onClick={onClick}
to={to}
as={to ? Link : 'button'}
target={target}
data-testid={dataTestId}
data-click-outside-id={dataClickOutsideId}
data-globally-prevent-click-outside={dataGloballyPreventClickOutside}
aria-label={ariaLabel}
type={type}
isLoading={isLoading}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
size={size}
style={dynamicStyles}
>
{(isLoading || Icon) && (
<ButtonIcon Icon={Icon} isLoading={!!isLoading} />
)}
{isDefined(title) && (
<ButtonText hasIcon={!!Icon} title={title} isLoading={isLoading} />
)}
{hotkeys && !isMobile && (
<ButtonHotkeys
hotkeys={hotkeys}
variant={variant}
accent={accent}
size={size}
/>
)}
{soon && <ButtonSoon />}
</StyledButton>
</StyledButtonWrapper>
);
};
@@ -1 +0,0 @@
export const baseTransitionTiming = 300;
@@ -1,71 +0,0 @@
import { styled } from '@linaria/react';
import {
type ButtonAccent,
type ButtonSize,
type ButtonVariant,
} from '@ui/input';
import { themeCssVariables } from '@ui/theme-constants';
import { getOsShortcutSeparator } from '@ui/utilities';
const StyledSeparator = styled.div<{
buttonSize: ButtonSize;
accent: ButtonAccent;
}>`
background: ${({ accent }) => {
switch (accent) {
case 'blue':
return themeCssVariables.buttons.secondaryTextColor;
case 'danger':
return themeCssVariables.border.color.danger;
default:
return themeCssVariables.font.color.light;
}
}};
height: ${({ buttonSize }) =>
buttonSize === 'small'
? themeCssVariables.spacing[2]
: themeCssVariables.spacing[4]};
margin: 0;
width: 1px;
`;
const StyledShortcutLabel = styled.div<{
variant: ButtonVariant;
accent: ButtonAccent;
}>`
color: ${({ variant, accent }) => {
switch (accent) {
case 'blue':
return themeCssVariables.buttons.secondaryTextColor;
case 'danger':
return variant === 'primary'
? themeCssVariables.border.color.danger
: themeCssVariables.color.red8;
default:
return themeCssVariables.font.color.light;
}
}};
font-weight: ${themeCssVariables.font.weight.medium};
`;
export const ButtonHotkeys = ({
size,
accent,
variant,
hotkeys,
}: {
size: ButtonSize;
accent: ButtonAccent;
variant: ButtonVariant;
hotkeys: string[];
}) => {
return (
<>
<StyledSeparator buttonSize={size} accent={accent} />
<StyledShortcutLabel variant={variant} accent={accent}>
{hotkeys.join(getOsShortcutSeparator())}
</StyledShortcutLabel>
</>
);
};
@@ -1,62 +0,0 @@
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-constants';
import { useContext } from 'react';
const StyledIcon = styled.div<{
isLoading: boolean;
}>`
align-items: center;
display: flex;
height: calc(100% - ${themeCssVariables.spacing[4]});
color: var(--tw-button-color);
opacity: ${({ isLoading }) => (isLoading ? 0 : 1)};
transition: opacity ${baseTransitionTiming / 2}ms ease;
transition-delay: ${({ isLoading }) =>
isLoading ? '0ms' : `${baseTransitionTiming / 2}ms`};
`;
const StyledIconWrapper = styled.div`
align-items: center;
display: flex;
pointer-events: none;
`;
const StyledLoader = styled.div`
left: ${themeCssVariables.spacing[2]};
opacity: 1;
position: absolute;
transition: opacity ${baseTransitionTiming / 2}ms ease;
transition-delay: ${baseTransitionTiming / 2}ms;
width: ${themeCssVariables.spacing[6]};
`;
export const ButtonIcon = ({
Icon,
isLoading,
}: {
Icon?: IconComponent;
isLoading?: boolean;
}) => {
const { theme } = useContext(ThemeContext);
return (
<StyledIconWrapper>
{isLoading && (
<StyledLoader>
<Loader />
</StyledLoader>
)}
{Icon && (
<StyledIcon isLoading={!!isLoading}>
<Icon size={theme.icon.size.sm} />
</StyledIcon>
)}
</StyledIconWrapper>
);
};
@@ -1,17 +0,0 @@
import { styled } from '@linaria/react';
import { Pill } from '@ui/components';
const StyledSoonPillContainer = styled.span`
display: flex;
margin-left: auto;
`;
type ButtonSoonProps = {
label?: string;
};
export const ButtonSoon = ({ label = 'Soon' }: ButtonSoonProps) => (
<StyledSoonPillContainer>
<Pill label={label} />
</StyledSoonPillContainer>
);
@@ -1,68 +0,0 @@
import { styled } from '@linaria/react';
import { baseTransitionTiming } from '@ui/input/button/components/Button/constant';
import { themeCssVariables } from '@ui/theme-constants';
const StyledEllipsis = styled.div<{ isLoading?: boolean }>`
right: 0;
clip-path: ${({ isLoading }) =>
isLoading
? `inset(0 0 0 0)`
: `inset(0 0 0 ${themeCssVariables.spacing[6]})`};
overflow: hidden;
position: absolute;
transition: clip-path ${baseTransitionTiming}ms ease;
`;
const StyledTextWrapper = styled.div`
align-items: center;
display: flex;
height: 100%;
justify-content: center;
position: relative;
`;
const StyledText = styled.div<{
isLoading?: boolean;
hasIcon: boolean;
}>`
clip-path: ${({ isLoading, hasIcon }) =>
isLoading
? ` inset(0 ${!hasIcon ? themeCssVariables.spacing[12] : themeCssVariables.spacing[6]} 0 0)`
: ' inset(0 0 0 0)'};
overflow: hidden;
transform: ${({ isLoading, hasIcon }) =>
isLoading
? `translateX(${!hasIcon ? themeCssVariables.spacing[7] : themeCssVariables.spacing[3]})`
: 'none'};
transition:
transform ${baseTransitionTiming}ms ease,
clip-path ${baseTransitionTiming}ms ease;
transition-delay: ${({ isLoading }) =>
isLoading ? '0ms' : `${baseTransitionTiming / 4}ms`};
white-space: nowrap;
`;
export const ButtonText = ({
hasIcon = false,
isLoading,
title,
}: {
isLoading?: boolean;
hasIcon: boolean;
title?: string;
}) => {
return (
<StyledTextWrapper>
<StyledText isLoading={isLoading} hasIcon={hasIcon}>
{title}
</StyledText>
<StyledEllipsis isLoading={isLoading}>...</StyledEllipsis>
</StyledTextWrapper>
);
};
@@ -1,60 +0,0 @@
import { styled } from '@linaria/react';
import React, { type ReactNode } from 'react';
import { type ButtonPosition, type ButtonProps } from './Button/Button';
import { themeCssVariables } from '@ui/theme-constants';
import { isDefined } from 'twenty-shared/utils';
const StyledButtonGroupContainer = styled.div`
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
`;
export type ButtonGroupProps = Partial<
Pick<ButtonProps, 'variant' | 'size' | 'accent'>
> & {
className?: string;
children: ReactNode[];
};
export const ButtonGroup = ({
className,
children,
variant,
size,
accent,
}: ButtonGroupProps) => {
return (
<StyledButtonGroupContainer className={className}>
{React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return null;
let position: ButtonPosition;
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
const additionalProps: any = { position, variant, accent, size };
if (isDefined(variant)) {
additionalProps.variant = variant;
}
if (isDefined(accent)) {
additionalProps.accent = accent;
}
if (isDefined(size)) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledButtonGroupContainer>
);
};
@@ -1,46 +0,0 @@
import { styled } from '@linaria/react';
import { ColorSample, type ColorSampleProps } from '@ui/display';
import { themeCssVariables } from '@ui/theme-constants';
import {
LightIconButton,
type LightIconButtonProps,
} from '@ui/input/button/components/LightIconButton';
type ColorPickerButtonProps = Pick<ColorSampleProps, 'colorName'> &
Pick<LightIconButtonProps, 'onClick'> & {
isSelected?: boolean;
};
const StyledButtonWrapper = styled.div<{
isSelected?: boolean;
}>`
button {
background-color: ${({ isSelected }) =>
isSelected
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
button:hover {
background-color: ${({ isSelected }) =>
isSelected
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
`;
export const ColorPickerButton = ({
colorName,
isSelected,
onClick,
}: ColorPickerButtonProps) => {
return (
<StyledButtonWrapper isSelected={isSelected}>
<LightIconButton
size="medium"
Icon={() => <ColorSample colorName={colorName} />}
onClick={onClick}
/>
</StyledButtonWrapper>
);
};
@@ -1,134 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { useContext } from 'react';
import { Link } from 'react-router-dom';
export type FloatingButtonSize = 'small' | 'medium';
export type FloatingButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type FloatingButtonProps = {
className?: string;
Icon?: IconComponent;
title?: string;
size?: FloatingButtonSize;
position?: FloatingButtonPosition;
applyShadow?: boolean;
applyBlur?: boolean;
disabled?: boolean;
focus?: boolean;
to?: string;
};
const StyledButton = styled.button<
Pick<
FloatingButtonProps,
| 'size'
| 'focus'
| 'position'
| 'applyBlur'
| 'applyShadow'
| 'position'
| 'to'
>
>`
align-items: center;
backdrop-filter: ${({ applyBlur }) => (applyBlur ? 'blur(20px)' : 'none')};
background: ${themeCssVariables.background.primary};
border: ${({ focus }) =>
focus ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return themeCssVariables.border.radius.sm;
}
return '';
}};
box-shadow: ${({ applyShadow, focus }) =>
applyShadow
? `0px 2px 4px 0px ${
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 ${themeCssVariables.color.blue3}`
: 'none'};
color: ${({ disabled, focus }) => {
return !disabled
? focus
? themeCssVariables.color.blue
: themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
padding: 0 ${themeCssVariables.spacing[2]};
transition: background 0.1s ease;
white-space: nowrap;
&:hover {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.lighter
: 'transparent'};
}
&:active {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
&:focus {
outline: none;
}
text-decoration: none;
`;
export const FloatingButton = ({
className,
Icon,
title,
size = 'small',
position = 'standalone',
applyBlur = true,
applyShadow = true,
disabled = false,
focus = false,
to,
}: FloatingButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
disabled={disabled}
focus={focus && !disabled}
size={size}
applyBlur={applyBlur}
applyShadow={applyShadow}
position={position}
className={className}
to={to}
as={to ? Link : 'button'}
>
{Icon && <Icon size={theme.icon.size.sm} />}
{title}
</StyledButton>
);
};
@@ -1,58 +0,0 @@
import { styled } from '@linaria/react';
import React from 'react';
import {
type FloatingButtonPosition,
type FloatingButtonProps,
} from './FloatingButton';
import { themeCssVariables } from '@ui/theme-constants';
import { isDefined } from 'twenty-shared/utils';
const StyledFloatingButtonGroupContainer = styled.div`
backdrop-filter: blur(20px);
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;
`;
export type FloatingButtonGroupProps = Pick<FloatingButtonProps, 'size'> & {
children: React.ReactElement[];
className?: string;
};
export const FloatingButtonGroup = ({
children,
size,
className,
}: 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';
}
const additionalProps: any = {
position,
size,
applyShadow: false,
applyBlur: false,
};
if (isDefined(size)) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledFloatingButtonGroupContainer>
);
};
@@ -1,136 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import React, { useContext } from 'react';
export type FloatingIconButtonSize = 'small' | 'medium';
export type FloatingIconButtonPosition =
| 'standalone'
| 'left'
| 'middle'
| 'right';
export type FloatingIconButtonProps = {
className?: string;
Icon?: IconComponent;
size?: FloatingIconButtonSize;
position?: FloatingIconButtonPosition;
applyShadow?: boolean;
applyBlur?: boolean;
disabled?: boolean;
focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
isActive?: boolean;
};
const StyledButton = styled.button<
Pick<
FloatingIconButtonProps,
'size' | 'position' | 'applyShadow' | 'applyBlur' | 'focus' | 'isActive'
>
>`
align-items: center;
backdrop-filter: ${({ applyBlur }) =>
applyBlur ? themeCssVariables.blur.medium : 'none'};
background: ${({ isActive }) =>
isActive
? themeCssVariables.background.transparent.medium
: themeCssVariables.background.primary};
border: ${({ focus }) =>
focus
? `1px solid ${themeCssVariables.color.blue}`
: `1px solid ${themeCssVariables.border.color.strong}`};
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return themeCssVariables.border.radius.sm;
}
return '';
}};
box-shadow: ${({ applyShadow, focus }) =>
applyShadow
? themeCssVariables.boxShadow.light
: focus
? `0 0 0 3px ${themeCssVariables.color.blue3}`
: 'none'};
box-sizing: border-box;
color: ${({ disabled, focus }) => {
return !disabled
? focus
? themeCssVariables.color.blue
: themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
justify-content: center;
padding: 0;
position: relative;
transition: background
calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
white-space: nowrap;
height: ${({ position, size }) =>
(size === 'small' ? 24 : 32) - (position === 'standalone' ? 0 : 4)}px;
width: ${({ position, size }) =>
(size === 'small' ? 24 : 32) - (position === 'standalone' ? 0 : 4)}px;
&:hover {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.lighter
: 'transparent'};
}
&:active {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
&:focus {
outline: none;
}
`;
export const FloatingIconButton = ({
className,
Icon,
size = 'small',
position = 'standalone',
applyShadow = true,
applyBlur = true,
disabled = false,
focus = false,
onClick,
isActive,
}: FloatingIconButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
disabled={disabled}
focus={focus && !disabled}
size={size}
applyShadow={applyShadow}
applyBlur={applyBlur}
className={className}
position={position}
onClick={onClick}
isActive={isActive}
>
{Icon && <Icon size={theme.icon.size.md} />}
</StyledButton>
);
};
@@ -1,67 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { type MouseEvent } from 'react';
import {
FloatingIconButton,
type FloatingIconButtonPosition,
type FloatingIconButtonProps,
} from './FloatingIconButton';
import { themeCssVariables } from '@ui/theme-constants';
const StyledFloatingIconButtonGroupContainer = styled.div`
backdrop-filter: blur(20px);
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;
`;
export type FloatingIconButtonGroupProps = Pick<
FloatingIconButtonProps,
'className' | 'size'
> & {
iconButtons: {
Icon: IconComponent;
onClick?: (event: MouseEvent<any>) => void;
isActive?: boolean;
}[];
};
export const FloatingIconButtonGroup = ({
iconButtons,
size,
className,
}: 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>
);
};
@@ -1,332 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import React, { useContext, useMemo } from 'react';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
export type IconButtonSize = 'medium' | 'small';
export type IconButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type IconButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type IconButtonAccent = 'default' | 'blue' | 'danger';
export type IconButtonProps = {
className?: string;
Icon?: IconComponent;
variant?: IconButtonVariant;
size?: IconButtonSize;
position?: IconButtonPosition;
accent?: IconButtonAccent;
disabled?: boolean;
focus?: boolean;
dataTestId?: string;
ariaLabel?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
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: themeCssVariables.grayScale.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: themeCssVariables.grayScale.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,
'variant' | 'size' | 'position' | 'accent' | 'focus' | 'to'
>
>`
align-items: center;
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);
}
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return `${themeCssVariables.border.radius.sm} 0px 0px ${themeCssVariables.border.radius.sm}`;
case 'right':
return `0px ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return themeCssVariables.border.radius.sm;
}
return '';
}};
border-style: solid;
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: ${themeCssVariables.font.family};
font-weight: 500;
gap: ${themeCssVariables.spacing[1]};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: center;
padding: 0;
transition: background 0.1s ease;
white-space: nowrap;
min-width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
&:focus {
outline: none;
}
`;
export const IconButton = ({
className,
Icon,
variant = 'primary',
size = 'medium',
accent = 'default',
position = 'standalone',
disabled = false,
focus = false,
dataTestId,
ariaLabel,
onClick,
to,
}: IconButtonProps) => {
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}
variant={variant}
size={size}
position={position}
disabled={disabled}
focus={focus}
accent={accent}
className={className}
onClick={onClick}
aria-label={ariaLabel}
to={to}
style={dynamicStyles}
>
{Icon && <Icon size={theme.icon.size.md} />}
</StyledButton>
);
};
@@ -1,54 +0,0 @@
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-constants';
export type IconButtonGroupProps = {
disabled?: boolean;
iconButtons: {
Icon: IconComponent;
onClick?: (event: MouseEvent<any>) => void;
}[];
className?: string;
};
const StyledIconButtonGroupContainer = styled.div<
Pick<IconButtonGroupProps, 'disabled'>
>`
display: inline-flex;
align-items: flex-start;
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: ${themeCssVariables.boxShadow.light};
}
`;
export const IconButtonGroup = ({
iconButtons,
disabled,
className,
}: IconButtonGroupProps) => {
return (
<StyledIconButtonGroupContainer className={className} disabled={disabled}>
{iconButtons.map(({ Icon, onClick }, index) => {
return (
<InsideButton
key={index}
Icon={Icon}
onClick={onClick}
disabled={disabled}
/>
);
})}
</StyledIconButtonGroupContainer>
);
};
@@ -1,47 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import React, { useContext } from 'react';
export type InsideButtonProps = {
className?: string;
Icon?: IconComponent;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
};
const StyledButton = styled.button`
align-items: center;
border: none;
background-color: transparent;
border-radius: ${themeCssVariables.border.radius.xs};
color: ${themeCssVariables.font.color.tertiary};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
height: 20px;
justify-content: center;
padding: 0;
white-space: nowrap;
min-width: 20px;
transition: background-color 0.1s ease;
&:hover {
background-color: ${themeCssVariables.background.transparent.light};
}
`;
export const InsideButton = ({
className,
Icon,
onClick,
disabled = false,
}: InsideButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton className={className} onClick={onClick} disabled={disabled}>
{Icon && <Icon size={theme.icon.size.sm} />}
</StyledButton>
);
};
@@ -1,108 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { type MouseEvent, useContext } from 'react';
export type LightButtonAccent = 'secondary' | 'tertiary';
export type LightButtonProps = {
className?: string;
Icon?: IconComponent;
title?: string;
accent?: LightButtonAccent;
active?: boolean;
disabled?: boolean;
focus?: boolean;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
type?: React.ComponentProps<'button'>['type'];
};
const StyledButton = styled.button<
Pick<LightButtonProps, 'accent' | 'active' | 'focus'>
>`
align-items: center;
background: transparent;
border: ${({ focus }) =>
focus ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
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
? themeCssVariables.color.blue
: !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
case 'tertiary':
return active || focus
? themeCssVariables.color.blue
: !disabled
? themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}
return '';
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: 24px;
padding: 0 ${themeCssVariables.spacing[2]};
transition: background 0.1s ease;
white-space: nowrap;
&:hover {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.light
: 'transparent'};
}
&:focus {
outline: none;
}
&:active {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
`;
export const LightButton = ({
className,
Icon,
title,
active = false,
accent = 'secondary',
disabled = false,
focus = false,
type = 'button',
onClick,
}: LightButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
onClick={onClick}
disabled={disabled}
focus={focus && !disabled}
type={type}
accent={accent}
className={className}
active={active}
>
{!!Icon && <Icon size={theme.icon.size.md} />}
{title}
</StyledButton>
);
};
@@ -1,130 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { type ComponentProps, type MouseEvent, useContext } from 'react';
export type LightIconButtonAccent = 'secondary' | 'tertiary';
export type LightIconButtonSize = 'small' | 'medium';
export type LightIconButtonProps = {
className?: string;
testId?: string;
Icon?: IconComponent;
title?: string;
size?: LightIconButtonSize;
accent?: LightIconButtonAccent;
active?: boolean;
disabled?: boolean;
focus?: boolean;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
} & Pick<ComponentProps<'button'>, 'aria-label' | 'title'>;
const StyledButton = styled.button<
Pick<LightIconButtonProps, 'accent' | 'active' | 'size' | 'focus'>
>`
align-items: center;
background: transparent;
border: none;
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
? themeCssVariables.color.blue
: !disabled
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.extraLight;
case 'tertiary':
return active || focus
? themeCssVariables.color.blue
: !disabled
? themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.extraLight;
}
return '';
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
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: ${themeCssVariables.spacing[1]};
transition: background 0.1s ease;
white-space: nowrap;
width: ${({ size }) =>
size === 'small'
? themeCssVariables.spacing[6]
: themeCssVariables.spacing[8]};
min-width: ${({ size }) =>
size === 'small'
? themeCssVariables.spacing[6]
: themeCssVariables.spacing[8]};
&:hover {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.light
: 'transparent'};
}
&:focus {
outline: none;
}
&:active {
background: ${({ disabled }) =>
!disabled
? themeCssVariables.background.transparent.medium
: 'transparent'};
}
`;
export const LightIconButton = ({
'aria-label': ariaLabel,
className,
testId,
Icon,
active = false,
size = 'small',
accent = 'secondary',
disabled = false,
focus = false,
onClick,
title,
}: LightIconButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
data-testid={testId}
aria-label={ariaLabel}
onClick={onClick}
disabled={disabled}
focus={focus && !disabled}
accent={accent}
className={className}
size={size}
active={active}
title={title}
>
{Icon && (
<Icon
size={size === 'medium' ? theme.icon.size.md : theme.icon.size.sm}
/>
)}
</StyledButton>
);
};
@@ -1,63 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import {
type FunctionComponent,
type MouseEvent,
type ReactElement,
} from 'react';
import { LightIconButton, type LightIconButtonProps } from './LightIconButton';
const StyledLightIconButtonGroupContainer = styled.div`
display: inline-flex;
gap: 2px;
`;
export type LightIconButtonGroupProps = Pick<
LightIconButtonProps,
'className' | 'size'
> & {
iconButtons: {
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
Icon: IconComponent;
accent?: LightIconButtonProps['accent'];
onClick?: (event: MouseEvent<any>) => void;
disabled?: boolean;
ariaLabel?: string;
dataTestId?: string;
}[];
};
export const LightIconButtonGroup = ({
iconButtons,
size,
className,
}: LightIconButtonGroupProps) => (
<StyledLightIconButtonGroupContainer className={className}>
{iconButtons.map(
({ Wrapper, Icon, accent, onClick, ariaLabel, dataTestId }, index) => {
const iconButton = (
<LightIconButton
key={`light-icon-button-${index}`}
Icon={Icon}
accent={accent}
disabled={!onClick}
onClick={onClick}
size={size}
aria-label={ariaLabel}
testId={dataTestId}
/>
);
return Wrapper ? (
<Wrapper
key={`light-icon-button-wrapper-${index}`}
iconButton={iconButton}
/>
) : (
iconButton
);
},
)}
</StyledLightIconButtonGroupContainer>
);
@@ -1,118 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import React, { type FunctionComponent, useContext } from 'react';
export type MainButtonVariant = 'primary' | 'secondary';
type Props = {
title: string;
fullWidth?: boolean;
width?: number;
variant?: MainButtonVariant;
soon?: boolean;
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<
Pick<Props, 'fullWidth' | 'width' | 'variant'>
>`
align-items: center;
background: ${({ variant, disabled }) => {
if (disabled === true) {
return themeCssVariables.background.secondary;
}
switch (variant) {
case 'primary':
return themeCssVariables.background.primaryInverted;
case 'secondary':
return themeCssVariables.background.primary;
default:
return themeCssVariables.background.primary;
}
}};
border: 1px solid;
border-color: ${({ disabled, variant }) => {
if (disabled === true) {
return themeCssVariables.background.transparent.lighter;
}
switch (variant) {
case 'primary':
return themeCssVariables.background.transparent.strong;
case 'secondary':
return themeCssVariables.border.color.medium;
default:
return themeCssVariables.background.primary;
}
}};
border-radius: ${themeCssVariables.border.radius.md};
box-shadow: ${({ disabled }) =>
disabled ? 'none' : themeCssVariables.boxShadow.light};
color: ${({ variant, disabled }) => {
if (disabled === true) {
return themeCssVariables.font.color.light;
}
switch (variant) {
case 'primary':
return themeCssVariables.font.color.inverted;
case 'secondary':
return themeCssVariables.font.color.primary;
default:
return themeCssVariables.font.color.primary;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.semiBold};
gap: ${themeCssVariables.spacing[2]};
justify-content: center;
outline: none;
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
max-height: ${themeCssVariables.spacing[8]};
width: ${({ fullWidth, width }) =>
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
&:hover {
background: ${({ variant, disabled }) => {
switch (variant) {
case 'secondary':
return themeCssVariables.background.tertiary;
default:
return !disabled
? themeCssVariables.background.primaryInvertedHover
: themeCssVariables.background.secondary;
}
}};
}
`;
type MainButtonProps = Props & {
Icon?: IconComponent | FunctionComponent<{ size: number }>;
};
export const MainButton = ({
Icon,
title,
width,
fullWidth = false,
variant = 'primary',
type,
onClick,
disabled,
className,
}: MainButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
className={className}
{...{ disabled, fullWidth, width, onClick, type, variant }}
>
{Icon && <Icon size={theme.icon.size.sm} />}
{title}
</StyledButton>
);
};
@@ -1,62 +0,0 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { useContext } from 'react';
export type RoundedIconButtonSize = 'small' | 'medium';
const StyledIconButton = styled.button<{
size: RoundedIconButtonSize;
}>`
align-items: center;
background: ${themeCssVariables.color.blue};
border: none;
border-radius: 50%;
color: ${themeCssVariables.font.color.inverted};
cursor: pointer;
display: flex;
height: ${({ size }) => (size === 'small' ? '20px' : '24px')};
justify-content: center;
outline: none;
padding: 0;
transition:
color 0.1s ease-in-out,
background 0.1s ease-in-out;
&:hover:not(:disabled) {
background: ${themeCssVariables.color.blue10};
}
&:disabled {
background: ${themeCssVariables.background.quaternary};
color: ${themeCssVariables.font.color.tertiary};
cursor: default;
}
width: ${({ size }) => (size === 'small' ? '20px' : '24px')};
`;
type RoundedIconButtonProps = {
Icon: IconComponent;
size?: RoundedIconButtonSize;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export const RoundedIconButton = ({
Icon,
onClick,
disabled,
className,
size = 'small',
}: RoundedIconButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledIconButton
className={className}
disabled={disabled}
onClick={onClick}
size={size}
>
<Icon size={theme.icon.size.md} />
</StyledIconButton>
);
};
@@ -1,82 +0,0 @@
import { styled } from '@linaria/react';
import { AppTooltip, type IconComponent, TooltipDelay } from '@ui/display';
import { StyledTabButton } from '@ui/input/button/components/TabButton/internals/components/StyledTabBase';
import { TabContent } from '@ui/input/button/components/TabButton/internals/components/TabContent';
import { type ReactElement } from 'react';
import { Link } from 'react-router-dom';
type TabButtonProps = {
id: string;
active?: boolean;
disabled?: boolean;
to?: string;
LeftIcon?: IconComponent;
className?: string;
title?: string;
onClick?: () => void;
logo?: string;
RightIcon?: IconComponent;
pill?: string | ReactElement;
contentSize?: 'sm' | 'md';
disableTestId?: boolean;
tooltipContent?: string;
};
const StyledTabTooltipWrapper = styled.div`
display: flex;
`;
export const TabButton = ({
id,
active,
disabled,
to,
LeftIcon,
className,
title,
onClick,
logo,
RightIcon,
pill,
contentSize = 'sm',
disableTestId = false,
tooltipContent,
}: TabButtonProps) => {
const tabElementId = `tab-${id}`;
return (
<StyledTabTooltipWrapper key={id} id={tabElementId}>
<StyledTabButton
data-testid={disableTestId ? undefined : `tab-${id}`}
active={active}
disabled={disabled}
as={to ? Link : 'button'}
to={to}
className={className}
onClick={onClick}
>
<TabContent
id={id}
active={active}
disabled={disabled}
LeftIcon={LeftIcon}
title={title}
logo={logo}
RightIcon={RightIcon}
pill={pill}
contentSize={contentSize}
/>
</StyledTabButton>
{tooltipContent && (
<AppTooltip
anchorSelect={`#${tabElementId}`}
content={tooltipContent}
noArrow
place="bottom"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
)}
</StyledTabTooltipWrapper>
);
};
@@ -1,92 +0,0 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from '@ui/theme-constants';
export const StyledTabButton = styled.button<{
active?: boolean;
disabled?: boolean;
to?: string;
}>`
all: unset;
align-items: center;
color: ${({ active, disabled }) =>
active
? themeCssVariables.font.color.primary
: disabled
? themeCssVariables.font.color.light
: themeCssVariables.font.color.secondary};
cursor: pointer;
background-color: transparent;
border: none;
font-family: inherit;
display: flex;
gap: ${themeCssVariables.spacing[1]};
justify-content: center;
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
text-decoration: none;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background-color: ${({ active }) =>
active ? themeCssVariables.border.color.inverted : 'transparent'};
z-index: 1;
}
`;
export const StyledTabContainer = styled.div<{
active?: boolean;
disabled?: boolean;
}>`
align-items: center;
color: ${({ active, disabled }) =>
active
? themeCssVariables.font.color.primary
: disabled
? themeCssVariables.font.color.light
: themeCssVariables.font.color.secondary};
cursor: pointer;
background-color: transparent;
display: flex;
gap: ${themeCssVariables.spacing[1]};
justify-content: center;
text-decoration: none;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background-color: ${({ active }) =>
active ? themeCssVariables.border.color.inverted : 'transparent'};
z-index: 1;
}
`;
export const StyledTabHover = styled.span<{
contentSize?: 'sm' | 'md';
}>`
box-sizing: border-box;
display: flex;
gap: ${themeCssVariables.spacing[1]};
padding: ${({ contentSize }) =>
contentSize === 'sm'
? `${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: ${themeCssVariables.border.radius.sm};
&:hover {
background: ${themeCssVariables.background.tertiary};
}
&:active {
background: ${themeCssVariables.background.quaternary};
}
`;
@@ -1,48 +0,0 @@
import { Pill } from '@ui/components/Pill/Pill';
import { Avatar, type IconComponent } from '@ui/display';
import { ThemeContext } from '@ui/theme-constants';
import { type ReactElement, useContext } from 'react';
import { StyledTabHover } from './StyledTabBase';
export type TabContentProps = {
id: string;
active?: boolean;
disabled?: boolean;
LeftIcon?: IconComponent;
title?: string;
logo?: string;
RightIcon?: IconComponent;
pill?: string | ReactElement;
contentSize?: 'sm' | 'md';
className?: string;
};
export const TabContent = ({
active,
disabled,
LeftIcon,
title,
logo,
RightIcon,
pill,
contentSize = 'sm',
className,
}: TabContentProps) => {
const { theme } = useContext(ThemeContext);
const iconColor = active
? theme.font.color.primary
: disabled
? theme.font.color.extraLight
: theme.font.color.secondary;
return (
<StyledTabHover contentSize={contentSize} className={className}>
{LeftIcon && <LeftIcon color={iconColor} size={theme.icon.size.md} />}
{logo && <Avatar avatarUrl={logo} size="md" placeholder={title} />}
{title}
{RightIcon && <RightIcon color={iconColor} size={theme.icon.size.md} />}
{pill && (typeof pill === 'string' ? <Pill label={pill} /> : pill)}
</StyledTabHover>
);
};
@@ -1,108 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconSearch } from '@ui/display';
import {
type LightIconButtonAccent,
type LightIconButtonSize,
} from '@ui/input/button/components/LightIconButton';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { AnimatedLightIconButton } from '../AnimatedLightIconButton';
const meta: Meta<typeof AnimatedLightIconButton> = {
title: 'UI/Input/Button/AnimatedLightIconButton',
component: AnimatedLightIconButton,
};
export default meta;
type Story = StoryObj<typeof AnimatedLightIconButton>;
export const Default: Story = {
args: {
title: 'Filter',
accent: 'secondary',
disabled: false,
active: false,
focus: false,
Icon: IconSearch,
animate: { scale: 1.2 },
transition: { duration: 0.3 },
},
argTypes: {
Icon: { control: false },
animate: { control: 'object' },
transition: { control: 'object' },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof AnimatedLightIconButton> = {
args: {
title: 'Filter',
Icon: IconSearch,
animate: { scale: 1.2 },
transition: { duration: 0.3 },
},
argTypes: {
accent: { control: false },
disabled: { control: false },
active: { control: false },
focus: { control: false },
animate: { control: 'object' },
transition: { control: 'object' },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'active',
'focus',
'disabled+focus',
'disabled+active',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { disabled: true, focus: true };
case 'disabled+active':
return { disabled: true, active: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['secondary', 'tertiary'] satisfies LightIconButtonAccent[],
props: (accent: LightIconButtonAccent) => ({ accent }),
},
{
name: 'sizes',
values: ['small', 'medium'] satisfies LightIconButtonSize[],
props: (size: LightIconButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,27 +0,0 @@
{/* Button.mdx */}
import { Meta, Controls, Story } from '@storybook/addon-docs/blocks';
import * as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
Button is a clickable interactive element that triggers a response.
You can place text and icons inside of a button.
Buttons are often used for form submissions and to toggle elements into view.
<Story of={ButtonStories.Default} />
<br />
## Properties
<Controls />
## Usage
```js
import { Button } from '@/ui/button/components/Button';
<Button title="Click me" />;
```
@@ -1,358 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconReload, IconSearch } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import {
Button,
type ButtonAccent,
type ButtonPosition,
type ButtonSize,
type ButtonVariant,
} from '../Button/Button';
const meta: Meta<typeof Button> = {
title: 'UI/Input/Button/Button',
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Default: Story = {
argTypes: {
hotkeys: { control: false },
Icon: { control: false },
},
args: {
title: 'Button',
size: 'small',
variant: 'primary',
inverted: false,
accent: 'danger',
disabled: false,
focus: false,
fullWidth: false,
soon: false,
position: 'standalone',
Icon: IconSearch,
className: '',
isLoading: false,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof Button> = {
args: { title: 'Filter', Icon: IconSearch, hotkeys: ['⌘', 'O'] },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
focus: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
className: { control: false },
isLoading: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies ButtonAccent[],
props: (accent: ButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const SoonCatalog: CatalogStory<Story, typeof Button> = {
args: { title: 'Filter', Icon: IconSearch, soon: true },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
focus: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
className: { control: false },
hotkeys: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies ButtonAccent[],
props: (accent: ButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const PositionCatalog: CatalogStory<Story, typeof Button> = {
args: { title: 'Filter', Icon: IconSearch },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
focus: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
hotkeys: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'positions',
values: [
'standalone',
'left',
'middle',
'right',
] satisfies ButtonPosition[],
props: (position: ButtonPosition) => ({ position }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const ShortcutCatalog: CatalogStory<Story, typeof Button> = {
args: { title: 'Actions', hotkeys: ['⌘', 'O'] },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
focus: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies ButtonAccent[],
props: (accent: ButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const FullWidth: Story = {
args: { title: 'Filter', Icon: IconSearch, fullWidth: true },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
focus: { control: false },
disabled: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
className: { control: false },
Icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const LoadingButton: Story = {
args: {
title: 'Reload',
Icon: IconReload,
isLoading: true,
},
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
focus: { control: false },
disabled: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
className: { control: false },
isLoading: { control: 'boolean' },
},
parameters: {
catalog: {
isLoading: [
{
name: 'isLoading',
values: [true, false] satisfies boolean[],
props: (value: boolean) => ({ isLoading: value }),
},
],
},
},
decorators: [ComponentDecorator],
};
@@ -1,82 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import {
Button,
type ButtonAccent,
type ButtonSize,
type ButtonVariant,
} from '../Button/Button';
import { ButtonGroup } from '../ButtonGroup';
const meta: Meta<typeof ButtonGroup> = {
title: 'UI/Input/Button/ButtonGroup',
component: ButtonGroup,
};
export default meta;
type Story = StoryObj<typeof ButtonGroup>;
export const Default: Story = {
args: {
size: 'small',
variant: 'primary',
accent: 'danger',
children: [
<Button key="note" Icon={IconNotes} title="Note" />,
<Button key="task" Icon={IconCheckbox} title="Task" />,
<Button key="activity" Icon={IconTimelineEvent} title="Activity" />,
],
},
argTypes: {
children: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof ButtonGroup> = {
args: {
children: [
<Button key="note" Icon={IconNotes} title="Note" />,
<Button key="task" Icon={IconCheckbox} title="Task" />,
<Button key="activity" Icon={IconTimelineEvent} title="Activity" />,
],
},
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
children: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies ButtonAccent[],
props: (accent: ButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,17 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ComponentDecorator } from '@ui/testing';
import { ColorPickerButton } from '../ColorPickerButton';
const meta: Meta<typeof ColorPickerButton> = {
title: 'UI/Input/Button/ColorPickerButton',
component: ColorPickerButton,
decorators: [ComponentDecorator],
args: { colorName: 'green' },
};
export default meta;
type Story = StoryObj<typeof ColorPickerButton>;
export const Default: Story = {};
export const Selected: Story = { args: { isSelected: true } };
@@ -1,84 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconSearch } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { FloatingButton, type FloatingButtonSize } from '../FloatingButton';
const meta: Meta<typeof FloatingButton> = {
title: 'UI/Input/Button/FloatingButton',
component: FloatingButton,
};
export default meta;
type Story = StoryObj<typeof FloatingButton>;
export const Default: Story = {
args: {
title: 'Filter',
size: 'small',
disabled: false,
focus: false,
applyBlur: true,
applyShadow: true,
position: 'standalone',
Icon: IconSearch,
},
argTypes: {
Icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof FloatingButton> = {
args: { title: 'Filter', Icon: IconSearch },
argTypes: {
size: { control: false },
disabled: { control: false },
position: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingButtonSize[],
props: (size: FloatingButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'disabled+focus':
return { disabled: true, focus: true };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,59 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { FloatingButton, type FloatingButtonSize } from '../FloatingButton';
import { FloatingButtonGroup } from '../FloatingButtonGroup';
const meta: Meta<typeof FloatingButtonGroup> = {
title: 'UI/Input/Button/FloatingButtonGroup',
component: FloatingButtonGroup,
};
export default meta;
type Story = StoryObj<typeof FloatingButtonGroup>;
export const Default: Story = {
args: {
size: 'small',
children: [
<FloatingButton key="notes" Icon={IconNotes} />,
<FloatingButton key="checkbox" Icon={IconCheckbox} />,
<FloatingButton key="timeline" Icon={IconTimelineEvent} />,
],
},
argTypes: {
children: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof FloatingButtonGroup> = {
args: {
children: [
<FloatingButton key="notes" Icon={IconNotes} />,
<FloatingButton key="checkbox" Icon={IconCheckbox} />,
<FloatingButton key="timeline" Icon={IconTimelineEvent} />,
],
},
argTypes: {
size: { control: false },
children: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingButtonSize[],
props: (size: FloatingButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,85 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconSearch } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import {
FloatingIconButton,
type FloatingIconButtonSize,
} from '../FloatingIconButton';
const meta: Meta<typeof FloatingIconButton> = {
title: 'UI/Input/Button/FloatingIconButton',
component: FloatingIconButton,
};
export default meta;
type Story = StoryObj<typeof FloatingIconButton>;
export const Default: Story = {
args: {
size: 'small',
disabled: false,
focus: false,
applyBlur: true,
applyShadow: true,
position: 'standalone',
Icon: IconSearch,
},
argTypes: {
Icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof FloatingIconButton> = {
args: { Icon: IconSearch },
argTypes: {
size: { control: false },
disabled: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingIconButtonSize[],
props: (size: FloatingIconButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'disabled+focus':
return { disabled: true, focus: true };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,53 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { type FloatingIconButtonSize } from '../FloatingIconButton';
import { FloatingIconButtonGroup } from '../FloatingIconButtonGroup';
const meta: Meta<typeof FloatingIconButtonGroup> = {
title: 'UI/Input/Button/FloatingIconButtonGroup',
component: FloatingIconButtonGroup,
args: {
iconButtons: [
{ Icon: IconNotes },
{ Icon: IconCheckbox },
{ Icon: IconTimelineEvent },
],
},
argTypes: {
iconButtons: { control: false },
},
};
export default meta;
type Story = StoryObj<typeof FloatingIconButtonGroup>;
export const Default: Story = {
args: {
size: 'small',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof FloatingIconButtonGroup> = {
argTypes: {
size: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingIconButtonSize[],
props: (size: FloatingIconButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,180 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconSearch } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import {
IconButton,
type IconButtonAccent,
type IconButtonPosition,
type IconButtonSize,
type IconButtonVariant,
} from '../IconButton';
const meta: Meta<typeof IconButton> = {
title: 'UI/Input/Button/IconButton',
component: IconButton,
};
export default meta;
type Story = StoryObj<typeof IconButton>;
export const Default: Story = {
args: {
size: 'small',
variant: 'primary',
accent: 'danger',
disabled: false,
focus: false,
position: 'standalone',
Icon: IconSearch,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof IconButton> = {
args: { Icon: IconSearch },
argTypes: {
size: { control: false },
variant: { control: false },
focus: { control: false },
accent: { control: false },
disabled: { control: false },
Icon: { control: false },
position: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies IconButtonSize[],
props: (size: IconButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies IconButtonAccent[],
props: (accent: IconButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies IconButtonVariant[],
props: (variant: IconButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const PositionCatalog: CatalogStory<Story, typeof IconButton> = {
args: { Icon: IconSearch },
argTypes: {
size: { control: false },
variant: { control: false },
focus: { control: false },
accent: { control: false },
disabled: { control: false },
position: { control: false },
Icon: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'positions',
values: [
'standalone',
'left',
'middle',
'right',
] satisfies IconButtonPosition[],
props: (position: IconButtonPosition) => ({ position }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'sizes',
values: ['small', 'medium'] satisfies IconButtonSize[],
props: (size: IconButtonSize) => ({ size }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies IconButtonVariant[],
props: (variant: IconButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,52 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { IconButtonGroup } from '../IconButtonGroup';
const meta: Meta<typeof IconButtonGroup> = {
title: 'UI/Input/Button/IconButtonGroup',
component: IconButtonGroup,
args: {
iconButtons: [
{ Icon: IconNotes },
{ Icon: IconCheckbox },
{ Icon: IconTimelineEvent },
],
},
argTypes: {
iconButtons: { control: false },
},
};
export default meta;
type Story = StoryObj<typeof IconButtonGroup>;
export const Default: Story = {
args: {
disabled: false,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof IconButtonGroup> = {
argTypes: {
disabled: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'disabled',
values: [true, false],
props: (disabled: boolean) => ({ disabled }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,88 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconSearch } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { LightButton, type LightButtonAccent } from '../LightButton';
const meta: Meta<typeof LightButton> = {
title: 'UI/Input/Button/LightButton',
component: LightButton,
};
export default meta;
type Story = StoryObj<typeof LightButton>;
export const Default: Story = {
args: {
title: 'Filter',
accent: 'secondary',
disabled: false,
active: false,
focus: false,
Icon: IconSearch,
},
argTypes: {
Icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof LightButton> = {
args: { title: 'Filter', Icon: IconSearch },
argTypes: {
accent: { control: false },
disabled: { control: false },
active: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'accents',
values: ['secondary', 'tertiary'] satisfies LightButtonAccent[],
props: (accent: LightButtonAccent) => ({ accent }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'active',
'focus',
'disabled+focus',
'disabled+active',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { disabled: true, focus: true };
case 'disabled+active':
return { disabled: true, active: true };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,97 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconSearch } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import {
LightIconButton,
type LightIconButtonAccent,
type LightIconButtonSize,
} from '../LightIconButton';
const meta: Meta<typeof LightIconButton> = {
title: 'UI/Input/Button/LightIconButton',
component: LightIconButton,
};
export default meta;
type Story = StoryObj<typeof LightIconButton>;
export const Default: Story = {
args: {
title: 'Filter',
accent: 'secondary',
disabled: false,
active: false,
focus: false,
Icon: IconSearch,
},
argTypes: {
Icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof LightIconButton> = {
args: { title: 'Filter', Icon: IconSearch },
argTypes: {
accent: { control: false },
disabled: { control: false },
active: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'active',
'focus',
'disabled+focus',
'disabled+active',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { disabled: true, focus: true };
case 'disabled+active':
return { disabled: true, active: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['secondary', 'tertiary'] satisfies LightIconButtonAccent[],
props: (accent: LightIconButtonAccent) => ({ accent }),
},
{
name: 'sizes',
values: ['small', 'medium'] satisfies LightIconButtonSize[],
props: (size: LightIconButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,61 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconBrandGoogle } from '@ui/display';
import { ComponentDecorator } from '@ui/testing';
import { expect, fn, userEvent, within } from 'storybook/test';
import { MainButton } from '../MainButton';
const clickJestFn = fn();
const meta: Meta<typeof MainButton> = {
title: 'UI/Input/Button/MainButton',
component: MainButton,
decorators: [ComponentDecorator],
args: { title: 'A primary Button', onClick: clickJestFn },
};
export default meta;
type Story = StoryObj<typeof MainButton>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};
export const WithIcon: Story = {
args: { Icon: IconBrandGoogle },
};
export const DisabledWithIcon: Story = {
args: { ...WithIcon.args, disabled: true },
};
export const FullWidth: Story = {
args: { fullWidth: true },
};
export const Width: Story = {
args: { width: 200 },
};
export const Secondary: Story = {
args: { title: 'A secondary Button', variant: 'secondary' },
};
export const SecondaryWithIcon: Story = {
args: { ...Secondary.args, ...WithIcon.args },
};
export const SecondaryDisabledWithIcon: Story = {
args: { ...SecondaryWithIcon.args, disabled: true },
};
export const SecondaryFullWidth: Story = {
args: { ...Secondary.args, ...FullWidth.args },
};
@@ -1,30 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconArrowRight } from '@ui/display';
import { ComponentDecorator } from '@ui/testing';
import { expect, fn, userEvent, within } from 'storybook/test';
import { RoundedIconButton } from '../RoundedIconButton';
const clickJestFn = fn();
const meta: Meta<typeof RoundedIconButton> = {
title: 'UI/Input/Button/RoundedIconButton',
component: RoundedIconButton,
};
export default meta;
type Story = StoryObj<typeof RoundedIconButton>;
export const Default: Story = {
decorators: [ComponentDecorator],
argTypes: { Icon: { control: false } },
args: { onClick: clickJestFn, Icon: IconArrowRight },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};
@@ -1,253 +0,0 @@
/* oxlint-disable react/jsx-props-no-spreading */
import { styled } from '@linaria/react';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import {
IconCheckbox,
IconChevronDown,
IconMail,
IconSearch,
IconSettings,
IconUser,
} from '@ui/display';
import { TabButton } from '@ui/input/button/components/TabButton/TabButton';
import {
CatalogDecorator,
type CatalogStory,
ComponentWithRouterDecorator,
JotaiRootDecorator,
} from '@ui/testing';
import { type ReactNode } from 'react';
import { themeCssVariables } from '@ui/theme-constants';
const StyledTabContainer = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[1]};
height: 40px;
user-select: none;
position: relative;
align-items: stretch;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
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,
decorators: [ComponentWithRouterDecorator, JotaiRootDecorator],
args: {
id: 'tab-button',
title: 'Tab Title',
active: false,
disabled: false,
contentSize: 'sm',
},
argTypes: {
LeftIcon: { control: false },
RightIcon: { control: false },
pill: { control: 'text' },
contentSize: {
control: 'select',
options: ['sm', 'md'],
},
},
};
export default meta;
type Story = StoryObj<typeof TabButton>;
export const Default: Story = {
args: {
title: 'General',
LeftIcon: IconSettings,
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const Active: Story = {
args: {
title: 'Active Tab',
LeftIcon: IconUser,
active: true,
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const Disabled: Story = {
args: {
title: 'Disabled Tab',
LeftIcon: IconCheckbox,
disabled: true,
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const WithLogo: Story = {
args: {
title: 'Company',
logo: 'https://picsum.photos/192/192',
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const WithStringPill: Story = {
args: {
title: 'Messages',
LeftIcon: IconMail,
pill: '12',
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const WithBothIcons: Story = {
args: {
title: 'Search',
LeftIcon: IconSearch,
RightIcon: IconChevronDown,
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const AsLink: Story = {
args: {
title: 'Link Tab',
LeftIcon: IconUser,
to: '/profile',
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const SmallContent: Story = {
args: {
title: 'Small',
LeftIcon: IconSettings,
contentSize: 'sm',
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const MediumContent: Story = {
args: {
title: 'Medium',
LeftIcon: IconSettings,
contentSize: 'md',
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
};
export const Catalog: CatalogStory<Story, typeof TabButton> = {
args: {
title: 'Tab title',
LeftIcon: IconCheckbox,
},
argTypes: {
active: { control: false },
disabled: { control: false },
onClick: { control: false },
to: { control: false },
},
render: (args) => (
<TabContainer>
<TabButton {...args} />
</TabContainer>
),
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },
catalog: {
dimensions: [
{
name: 'states',
values: ['default', 'hover', 'active'],
props: (state: string) =>
state === 'default' ? {} : { className: state },
},
{
name: 'State',
values: ['active', 'inactive', 'disabled'],
labels: (state: string) => state,
props: (state: string) => ({
active: state === 'active',
disabled: state === 'disabled',
}),
},
{
name: 'Content Size',
values: ['sm', 'md'],
labels: (size: string) => size,
props: (size: string) => ({ contentSize: size as 'sm' | 'md' }),
},
{
name: 'Content',
values: ['icon', 'logo', 'pill'],
props: (content: string) => {
switch (content) {
case 'icon':
return { LeftIcon: IconSettings };
case 'logo':
return {
logo: 'https://picsum.photos/192/192',
LeftIcon: undefined,
};
case 'pill':
return { LeftIcon: IconMail, pill: '5' };
default:
return {};
}
},
},
],
},
layout: 'centered',
viewport: {
defaultViewport: 'responsive',
},
},
decorators: [CatalogDecorator],
};
@@ -1,238 +0,0 @@
import { styled } from '@linaria/react';
import Editor, { type EditorProps, type Monaco } from '@monaco-editor/react';
import { Loader } from '@ui/feedback/loader/components/Loader';
import { ResizeHandle } from '@ui/layout/resize-handle/components/ResizeHandle';
import { BASE_CODE_EDITOR_THEME_ID } from '@ui/input/code-editor/constants/BaseCodeEditorThemeId';
import { useResizeHandle } from '@ui/layout/resize-handle/hooks/useResizeHandle';
import { getBaseCodeEditorTheme } from '@ui/input/code-editor/theme/utils/getBaseCodeEditorTheme';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { type editor } from 'monaco-editor';
import { type KeyboardEvent, useContext, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
type CodeEditorVariant = 'default' | 'with-header' | 'borderless';
type CodeEditorProps = Pick<
EditorProps,
'value' | 'language' | 'onMount' | 'onValidate' | 'height' | 'options'
> & {
onChange?: (value: string) => void;
setMarkers?: (value: string) => editor.IMarkerData[];
variant?: CodeEditorVariant;
isLoading?: boolean;
transparentBackground?: boolean;
resizable?: boolean;
};
const StyledEditorLoader = styled.div<{
height: string | number;
variant: CodeEditorVariant;
}>`
align-items: center;
display: flex;
height: ${({ height }) =>
typeof height === 'number' ? `${height}px` : height};
justify-content: center;
border: ${({ variant }) =>
variant === 'borderless'
? 'none'
: `1px solid ${themeCssVariables.border.color.medium}`};
border-top: ${({ variant }) => {
if (variant === 'default')
return `1px solid ${themeCssVariables.border.color.medium}`;
return 'none';
}};
border-radius: ${({ variant }) => {
switch (variant) {
case 'default':
return themeCssVariables.border.radius.sm;
case 'with-header':
return `0 0 ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm}`;
default:
return '0';
}
}};
background-color: ${themeCssVariables.background.transparent.lighter};
`;
const StyledCodeEditorContainer = styled.div`
display: contents;
`;
const StyledEditorWrapper = styled.div<{
variant: CodeEditorVariant;
transparentBackground?: boolean;
}>`
display: contents;
.monaco-editor {
outline-width: 0;
background-color: ${({ transparentBackground }) =>
!transparentBackground
? themeCssVariables.background.secondary
: 'transparent'};
border-radius: ${({ variant }) =>
variant !== 'borderless' ? themeCssVariables.border.radius.sm : '0'};
}
.overflow-guard {
box-sizing: border-box;
border: ${({ variant }) => {
switch (variant) {
case 'default':
case 'with-header':
return `1px solid ${themeCssVariables.border.color.medium}`;
default:
return 'none';
}
}};
border-radius: ${({ variant }) => {
switch (variant) {
case 'default':
return themeCssVariables.border.radius.sm;
case 'with-header':
return `0 0 ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm}`;
default:
return '0';
}
}};
border-top: ${({ variant }) => {
if (variant === 'with-header') return 'none';
if (variant === 'default')
return `1px solid ${themeCssVariables.border.color.medium}`;
return 'none';
}};
}
`;
export const CodeEditor = ({
value,
language,
onMount,
onChange,
setMarkers,
onValidate,
height = 450,
variant = 'default',
transparentBackground,
isLoading = false,
options,
resizable = false,
}: CodeEditorProps) => {
const { theme } = useContext(ThemeContext);
const [monaco, setMonaco] = useState<Monaco | undefined>(undefined);
const [editor, setEditor] = useState<
editor.IStandaloneCodeEditor | undefined
>(undefined);
const [isEditorFocused, setIsEditorFocused] = useState(false);
const numericHeight = typeof height === 'number' ? height : 450;
const {
size: resizableHeight,
handleResizeStart,
handleResizeMove,
handleResizeEnd,
} = useResizeHandle({
initialSize: numericHeight,
});
const currentHeight = resizable ? resizableHeight : height;
const setModelMarkers = (
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined,
) => {
const model = editor?.getModel();
if (!isDefined(model)) {
return;
}
const customMarkers = setMarkers?.(model.getValue());
if (isDefined(customMarkers)) {
monaco?.editor.setModelMarkers(model, 'customMarker', customMarkers);
}
};
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (isEditorFocused) {
event.stopPropagation();
}
};
return isLoading ? (
<StyledEditorLoader height={currentHeight} variant={variant}>
<Loader />
</StyledEditorLoader>
) : (
<StyledCodeEditorContainer onKeyDown={handleKeyDown}>
<input
type="hidden"
data-testid="code-editor-value"
value={value ?? ''}
readOnly
/>
<StyledEditorWrapper
variant={variant}
transparentBackground={transparentBackground}
>
<Editor
height={currentHeight}
value={value}
language={language}
loading=""
onMount={(editor, monaco) => {
setMonaco(monaco);
setEditor(editor);
monaco.editor.defineTheme(
BASE_CODE_EDITOR_THEME_ID,
getBaseCodeEditorTheme(theme),
);
monaco.editor.setTheme(BASE_CODE_EDITOR_THEME_ID);
editor.onDidFocusEditorWidget(() => {
setIsEditorFocused(true);
});
editor.onDidBlurEditorWidget(() => {
setIsEditorFocused(false);
});
onMount?.(editor, monaco);
setModelMarkers(editor, monaco);
}}
onChange={(value) => {
if (isDefined(value)) {
onChange?.(value);
setModelMarkers(editor, monaco);
}
}}
onValidate={(markers) => {
onValidate?.(markers);
}}
options={{
formatOnPaste: true,
formatOnType: true,
overviewRulerLanes: 0,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
minimap: {
enabled: false,
},
...options,
}}
/>
</StyledEditorWrapper>
{resizable && (
<ResizeHandle
onPointerDown={handleResizeStart}
onPointerMove={handleResizeMove}
onPointerUp={handleResizeEnd}
/>
)}
</StyledCodeEditorContainer>
);
};
@@ -1,53 +0,0 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from '@ui/theme-constants';
const StyledEditorHeader = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.transparent.lighter};
color: ${themeCssVariables.font.color.tertiary};
font-weight: ${themeCssVariables.font.weight.medium};
display: flex;
height: ${themeCssVariables.spacing[10]};
padding: 0 ${themeCssVariables.spacing[2]};
border: 1px solid ${themeCssVariables.border.color.medium};
border-top-left-radius: ${themeCssVariables.border.radius.sm};
border-top-right-radius: ${themeCssVariables.border.radius.sm};
justify-content: space-between;
`;
const StyledElementContainer = styled.div`
align-content: flex-end;
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
export type CoreEditorHeaderProps = {
title?: string;
leftNodes?: React.ReactNode[];
rightNodes?: React.ReactNode[];
};
export const CoreEditorHeader = ({
title,
leftNodes,
rightNodes,
}: CoreEditorHeaderProps) => {
return (
<StyledEditorHeader>
<StyledElementContainer>
{leftNodes &&
leftNodes.map((leftButton, index) => {
return <div key={`left-${index}`}>{leftButton}</div>;
})}
{title}
</StyledElementContainer>
<StyledElementContainer>
{rightNodes &&
rightNodes.map((rightButton, index) => {
return <div key={`right-${index}`}>{rightButton}</div>;
})}
</StyledElementContainer>
</StyledEditorHeader>
);
};
@@ -1 +0,0 @@
export const BASE_CODE_EDITOR_THEME_ID = 'baseCodeEditorTheme';
@@ -1,3 +0,0 @@
export * from './components/CodeEditor';
export * from './components/CodeEditorHeader';
export * from './theme/utils/getBaseCodeEditorTheme';
@@ -1,77 +0,0 @@
import { type ThemeType } from '@ui/theme-constants';
import { type editor } from 'monaco-editor';
import { isDefined } from 'twenty-shared/utils';
const convertColorToHex = (color: string): string => {
const displayP3Match = color.match(
/color\(display-p3\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s+\/\s+([\d.]+))?\)/,
);
if (isDefined(displayP3Match)) {
const [, r, g, b, a] = displayP3Match;
const rHex = Math.round(parseFloat(r) * 255)
.toString(16)
.padStart(2, '0');
const gHex = Math.round(parseFloat(g) * 255)
.toString(16)
.padStart(2, '0');
const bHex = Math.round(parseFloat(b) * 255)
.toString(16)
.padStart(2, '0');
if (isDefined(a)) {
const aHex = Math.round(parseFloat(a) * 255)
.toString(16)
.padStart(2, '0');
return `#${rHex}${gHex}${bHex}${aHex}`;
}
return `#${rHex}${gHex}${bHex}`;
}
return color;
};
export const getBaseCodeEditorTheme = (
theme: ThemeType,
): editor.IStandaloneThemeData => {
return {
base: 'vs',
inherit: true,
rules: [
{
token: '',
foreground: convertColorToHex(theme.code.text.gray),
fontStyle: 'bold',
},
{
token: 'keyword',
foreground: convertColorToHex(theme.code.text.sky),
},
{
token: 'delimiter',
foreground: convertColorToHex(theme.code.text.gray),
},
{
token: 'string',
foreground: convertColorToHex(theme.code.text.pink),
},
{
token: 'comment',
foreground: convertColorToHex(theme.code.text.orange),
},
],
colors: {
'editor.background': '#00000000',
'editorCursor.foreground': convertColorToHex(theme.font.color.primary),
'editorLineNumber.foreground': convertColorToHex(
theme.font.color.extraLight,
),
'editorLineNumber.activeForeground': convertColorToHex(
theme.font.color.light,
),
'editor.lineHighlightBackground': convertColorToHex(
theme.background.tertiary,
),
},
};
};
@@ -1,248 +0,0 @@
import { styled } from '@linaria/react';
import { Checkmark } from '@ui/display/checkmark/components/Checkmark';
import { type ColorScheme } from '@ui/input/types/ColorScheme';
import {
AnimatePresence,
type AnimationControls,
motion,
useAnimation,
} from 'framer-motion';
import React from 'react';
import { GRAY_SCALE_DARK } from '@ui/theme/constants/GrayScaleDark';
import { GRAY_SCALE_LIGHT } from '@ui/theme/constants/GrayScaleLight';
import { themeCssVariables } from '@ui/theme-constants';
const StyledColorSchemeBackground = styled.div<
Pick<ColorSchemeCardProps, 'variant'>
>`
align-items: flex-end;
background: ${({ variant }) => {
switch (variant) {
case 'Dark':
return GRAY_SCALE_DARK.gray4;
case 'Light':
default:
return GRAY_SCALE_LIGHT.gray4;
}
}};
border: ${({ variant }) => {
switch (variant) {
case 'Dark':
return `1px solid ${GRAY_SCALE_DARK.gray5}`;
case 'Light':
default:
return `1px solid ${GRAY_SCALE_LIGHT.gray5}`;
}
}};
border-radius: ${themeCssVariables.border.radius.md};
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 80px;
justify-content: flex-end;
overflow: hidden;
padding-left: ${themeCssVariables.spacing[6]};
padding-top: ${themeCssVariables.spacing[6]};
width: 160px;
`;
const StyledColorSchemeContentBase = styled.div<
Pick<ColorSchemeCardProps, 'variant'>
>`
background: ${({ variant }) => {
switch (variant) {
case 'Dark':
return GRAY_SCALE_DARK.gray1;
case 'Light':
return GRAY_SCALE_LIGHT.gray1;
}
return '';
}};
border-left: ${({ variant }) => {
switch (variant) {
case 'Dark':
return `1px solid ${GRAY_SCALE_DARK.gray5}`;
case 'Light':
default:
return `1px solid ${GRAY_SCALE_LIGHT.gray5}`;
}
}};
border-radius: ${themeCssVariables.border.radius.md} 0px 0px 0px;
border-top: ${({ variant }) => {
switch (variant) {
case 'Dark':
return `1px solid ${GRAY_SCALE_DARK.gray5}`;
case 'Light':
default:
return `1px solid ${GRAY_SCALE_LIGHT.gray5}`;
}
}};
box-sizing: border-box;
color: ${({ variant }) => {
switch (variant) {
case 'Dark':
return GRAY_SCALE_DARK.gray12;
case 'Light':
default:
return GRAY_SCALE_LIGHT.gray12;
}
}};
display: flex;
flex: 1;
font-size: 20px;
height: 56px;
padding-left: ${themeCssVariables.spacing[2]};
padding-top: ${themeCssVariables.spacing[2]};
`;
const StyledColorSchemeContent = motion.create(StyledColorSchemeContentBase);
export type ColorSchemeSegmentProps = {
variant: ColorScheme;
controls: AnimationControls;
className?: string;
} & React.ComponentPropsWithoutRef<'div'>;
const ColorSchemeSegment = ({
variant,
controls,
style,
className,
onClick,
onMouseEnter,
onMouseLeave,
}: ColorSchemeSegmentProps) => {
return (
<StyledColorSchemeBackground
className={className}
{...{ variant, style, onClick, onMouseEnter, onMouseLeave }}
>
<StyledColorSchemeContent animate={controls} variant={variant}>
Aa
</StyledColorSchemeContent>
</StyledColorSchemeBackground>
);
};
const StyledContainer = styled.div`
position: relative;
width: 160px;
`;
const StyledMixedColorSchemeSegment = styled.div`
border-radius: ${themeCssVariables.border.radius.md};
cursor: pointer;
display: flex;
height: 80px;
overflow: hidden;
position: relative;
width: 160px;
`;
const StyledCheckmarkContainerBase = styled.div`
bottom: 0px;
padding: ${themeCssVariables.spacing[2]};
position: absolute;
right: 0px;
`;
const StyledCheckmarkContainer = motion.create(StyledCheckmarkContainerBase);
export type ColorSchemeCardProps = {
variant: ColorScheme;
selected?: boolean;
} & React.ComponentPropsWithoutRef<'div'>;
const checkmarkAnimationVariants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
export const ColorSchemeCard = ({
variant,
selected,
onClick,
}: ColorSchemeCardProps) => {
const controls = useAnimation();
const handleMouseEnter = () => {
controls.start({
height: 61,
fontSize: '22px',
transition: { duration: 0.1 },
});
};
const handleMouseLeave = () => {
controls.start({
height: 56,
fontSize: '20px',
transition: { duration: 0.1 },
});
};
if (variant === 'System') {
return (
<StyledContainer>
<StyledMixedColorSchemeSegment
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={onClick}
>
<ColorSchemeSegment
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
controls={controls}
variant="Light"
/>
<ColorSchemeSegment
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
controls={controls}
variant="Dark"
/>
</StyledMixedColorSchemeSegment>
<AnimatePresence>
{selected && (
<StyledCheckmarkContainer
key="system"
variants={checkmarkAnimationVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.3 }}
>
<Checkmark />
</StyledCheckmarkContainer>
)}
</AnimatePresence>
</StyledContainer>
);
}
return (
<StyledContainer>
<ColorSchemeSegment
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
controls={controls}
variant={variant}
onClick={onClick}
/>
<AnimatePresence>
{selected && (
<StyledCheckmarkContainer
key={variant}
variants={checkmarkAnimationVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.3 }}
>
<Checkmark />
</StyledCheckmarkContainer>
)}
</AnimatePresence>
</StyledContainer>
);
};
@@ -1,75 +0,0 @@
import { styled } from '@linaria/react';
import { type ColorScheme } from '@ui/input/types/ColorScheme';
import { MOBILE_VIEWPORT, themeCssVariables } from '@ui/theme-constants';
import { ColorSchemeCard } from './ColorSchemeCard';
const StyledContainer = styled.div`
display: flex;
flex-direction: row;
> * + * {
margin-left: ${themeCssVariables.spacing[4]};
}
@media (max-width: ${MOBILE_VIEWPORT}px) {
overflow: scroll;
}
`;
const StyledCardContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledLabel = styled.span`
color: ${themeCssVariables.font.color.secondary};
font-size: ${themeCssVariables.font.size.xs};
font-weight: ${themeCssVariables.font.weight.medium};
margin-top: ${themeCssVariables.spacing[2]};
`;
export type ColorSchemePickerProps = {
value: ColorScheme;
className?: string;
onChange: (value: ColorScheme) => void;
lightLabel: string;
darkLabel: string;
systemLabel: string;
};
export const ColorSchemePicker = ({
value,
onChange,
className,
lightLabel,
darkLabel,
systemLabel,
}: ColorSchemePickerProps) => {
return (
<StyledContainer className={className}>
<StyledCardContainer>
<ColorSchemeCard
onClick={() => onChange('Light')}
variant="Light"
selected={value === 'Light'}
/>
<StyledLabel>{lightLabel}</StyledLabel>
</StyledCardContainer>
<StyledCardContainer>
<ColorSchemeCard
onClick={() => onChange('Dark')}
variant="Dark"
selected={value === 'Dark'}
/>
<StyledLabel>{darkLabel}</StyledLabel>
</StyledCardContainer>
<StyledCardContainer>
<ColorSchemeCard
onClick={() => onChange('System')}
variant="System"
selected={value === 'System'}
/>
<StyledLabel>{systemLabel}</StyledLabel>
</StyledCardContainer>
</StyledContainer>
);
};
@@ -1,51 +0,0 @@
import { styled } from '@linaria/react';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ComponentDecorator } from '@ui/testing';
import { themeCssVariables } from '@ui/theme-constants';
import { ColorSchemeCard } from '../ColorSchemeCard';
const StyledContainer = styled.div`
display: flex;
flex-direction: row;
> * + * {
margin-left: ${themeCssVariables.spacing['4']};
}
`;
const meta: Meta<typeof ColorSchemeCard> = {
title: 'UI/Input/ColorScheme/ColorSchemeCard',
component: ColorSchemeCard,
decorators: [
(Story) => {
return (
<StyledContainer>
<Story />
</StyledContainer>
);
},
ComponentDecorator,
],
argTypes: {
variant: { control: false },
},
args: { selected: false },
};
export default meta;
type Story = StoryObj<typeof ColorSchemeCard>;
export const Default: Story = {
render: (args) => (
<>
<ColorSchemeCard variant="Light" selected={args.selected} />
<ColorSchemeCard variant="Dark" selected={args.selected} />
<ColorSchemeCard variant="System" selected={args.selected} />
</>
),
};
export const Selected: Story = {
...Default,
args: { selected: true },
};
@@ -1,53 +0,0 @@
import { styled } from '@linaria/react';
import React from 'react';
import { themeCssVariables } from '@ui/theme-constants';
import { Radio } from './Radio';
const StyledSubscriptionCardContainer = styled.button`
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
cursor: pointer;
display: flex;
padding: 0;
position: relative;
text-align: left;
width: 100%;
:hover {
background: ${themeCssVariables.background.tertiary};
}
`;
const StyledCardInner = styled.div`
display: flex;
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]};
width: 100%;
`;
const StyledRadioContainer = styled.div`
position: absolute;
right: ${themeCssVariables.spacing[2]};
top: ${themeCssVariables.spacing[2]};
`;
type CardPickerProps = {
children: React.ReactNode;
handleChange?: () => void;
checked?: boolean;
};
export const CardPicker = ({
children,
checked,
handleChange,
}: CardPickerProps) => {
return (
<StyledSubscriptionCardContainer onClick={handleChange}>
<StyledRadioContainer>
<Radio checked={checked} />
</StyledRadioContainer>
<StyledCardInner>{children}</StyledCardInner>
</StyledSubscriptionCardContainer>
);
};
@@ -1,248 +0,0 @@
import { styled } from '@linaria/react';
import { IconCheck, IconMinus } from '@ui/display/icon/components/TablerIcons';
import { themeCssVariables } from '@ui/theme-constants';
import * as React from 'react';
export enum CheckboxVariant {
Primary = 'primary',
Secondary = 'secondary',
Tertiary = 'tertiary',
}
export enum CheckboxShape {
Squared = 'squared',
Rounded = 'rounded',
}
export enum CheckboxSize {
Large = 'large',
Small = 'small',
}
export enum CheckboxAccent {
Blue = 'blue',
Orange = 'orange',
}
type CheckboxProps = {
checked: boolean;
indeterminate?: boolean;
hoverable?: boolean;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onCheckedChange?: (value: boolean) => void;
variant?: CheckboxVariant;
size?: CheckboxSize;
shape?: CheckboxShape;
className?: string;
disabled?: boolean;
accent?: CheckboxAccent;
};
type InputProps = {
checkboxSize: CheckboxSize;
variant: CheckboxVariant;
accent?: CheckboxAccent;
indeterminate?: boolean;
hoverable?: boolean;
shape?: CheckboxShape;
isChecked?: boolean;
disabled?: boolean;
};
const StyledCheckboxContainer = styled.div<InputProps>`
--checkbox-outer-size: ${({ checkboxSize, hoverable }) => {
if (hoverable === true) {
return checkboxSize === CheckboxSize.Large ? '32px' : '24px';
} else {
return checkboxSize === CheckboxSize.Large ? '20px' : '14px';
}
}};
--checkbox-label-size: ${({ checkboxSize }) =>
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
--checkbox-icon-size: ${({ checkboxSize }) =>
checkboxSize === CheckboxSize.Large ? '20px' : '14px'};
--checkbox-bg: ${({ indeterminate, isChecked, disabled, accent }) => {
if (!(indeterminate || isChecked)) return 'transparent';
return disabled === true
? accent === CheckboxAccent.Blue
? themeCssVariables.color.blue7
: themeCssVariables.color.orange7
: accent === CheckboxAccent.Blue
? themeCssVariables.color.blue
: themeCssVariables.color.orange;
}};
--checkbox-border-color: ${({
indeterminate,
isChecked,
variant,
disabled,
accent,
}) => {
switch (true) {
case indeterminate || isChecked:
return disabled === true
? accent === CheckboxAccent.Blue
? themeCssVariables.color.blue7
: themeCssVariables.color.orange7
: accent === CheckboxAccent.Blue
? themeCssVariables.color.blue
: themeCssVariables.color.orange;
case disabled === true:
return themeCssVariables.border.color.strong;
case variant === CheckboxVariant.Primary:
return themeCssVariables.border.color.inverted;
case variant === CheckboxVariant.Tertiary:
return themeCssVariables.border.color.medium;
default:
return themeCssVariables.border.color.secondaryInverted;
}
}};
--checkbox-border-radius: ${({ shape }) =>
shape === CheckboxShape.Rounded
? themeCssVariables.border.radius.rounded
: themeCssVariables.border.radius.sm};
--checkbox-border-width: ${({ variant, checkboxSize }) =>
checkboxSize === CheckboxSize.Large || variant === CheckboxVariant.Tertiary
? '1.43px'
: '1px'};
--checkbox-cursor: ${({ disabled }) =>
disabled === true ? 'not-allowed' : 'pointer'};
--checkbox-stroke: ${themeCssVariables.font.color.inverted};
align-items: center;
border-radius: ${({ shape }) =>
shape === CheckboxShape.Rounded
? themeCssVariables.border.radius.rounded
: themeCssVariables.border.radius.md};
cursor: var(--checkbox-cursor);
display: flex;
padding: ${({ checkboxSize, hoverable }) => {
if (hoverable === true) {
return checkboxSize === CheckboxSize.Large ? '6px' : '5px';
} else {
return '0';
}
}};
position: relative;
&:hover {
background-color: ${({
hoverable,
isChecked,
indeterminate,
disabled,
accent,
}) => {
if (hoverable !== true || disabled === true) return 'transparent';
if (indeterminate === true || isChecked === true) {
return accent === CheckboxAccent.Blue
? themeCssVariables.background.transparent.blue
: themeCssVariables.background.transparent.orange;
}
return themeCssVariables.background.transparent.light;
}};
}
input + label {
cursor: var(--checkbox-cursor);
height: calc(var(--checkbox-label-size) + 2px);
padding: 0;
position: relative;
width: calc(var(--checkbox-label-size) + 2px);
}
input + label:before {
background: var(--checkbox-bg);
border-color: var(--checkbox-border-color);
border-radius: var(--checkbox-border-radius);
border-style: solid;
border-width: var(--checkbox-border-width);
content: '';
cursor: var(--checkbox-cursor);
display: inline-block;
height: var(--checkbox-label-size);
width: var(--checkbox-label-size);
}
input + label > svg {
--padding: 0px;
height: var(--checkbox-icon-size);
left: var(--padding);
position: absolute;
stroke: var(--checkbox-stroke);
top: var(--padding);
width: var(--checkbox-icon-size);
}
`;
const StyledCheckboxInput = styled.input`
cursor: ${({ disabled }) => (disabled === true ? 'not-allowed' : 'pointer')};
margin: 0;
opacity: 0;
position: absolute;
z-index: 10;
`;
export const Checkbox = ({
checked,
onChange,
onCheckedChange,
indeterminate,
variant = CheckboxVariant.Primary,
size = CheckboxSize.Small,
shape = CheckboxShape.Squared,
hoverable = true,
className,
disabled = false,
accent = CheckboxAccent.Blue,
}: CheckboxProps) => {
const [isInternalChecked, setIsInternalChecked] =
React.useState<boolean>(false);
React.useEffect(() => {
setIsInternalChecked(checked ?? false);
}, [checked]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(event);
onCheckedChange?.(event.target.checked);
setIsInternalChecked(event.target.checked ?? false);
};
const checkboxId = React.useId();
return (
<StyledCheckboxContainer
checkboxSize={size}
variant={variant}
shape={shape}
isChecked={isInternalChecked}
hoverable={hoverable}
indeterminate={indeterminate}
className={className}
disabled={disabled}
accent={accent}
>
<StyledCheckboxInput
autoComplete="off"
type="checkbox"
id={checkboxId}
name="styled-checkbox"
data-testid="input-checkbox"
checked={isInternalChecked}
onChange={handleChange}
disabled={disabled}
/>
<label htmlFor={checkboxId}>
{indeterminate ? (
<IconMinus />
) : isInternalChecked ? (
<IconCheck />
) : (
<></>
)}
</label>
</StyledCheckboxContainer>
);
};
@@ -1,11 +0,0 @@
import IconListViewGripRaw from '@assets/misc/list-view-grip.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconListViewGripProps = Pick<IconComponentProps, 'size' | 'stroke'>;
export const IconListViewGrip = (props: IconListViewGripProps) => {
const width = props.size ?? 8;
const height = props.size ?? 32;
return <IconListViewGripRaw height={height} width={width} />;
};
@@ -1,175 +0,0 @@
import { styled } from '@linaria/react';
import { motion } from 'framer-motion';
import * as React from 'react';
import { themeCssVariables } from '@ui/theme-constants';
import { RadioGroup } from './RadioGroup';
export enum RadioSize {
Large = 'large',
Small = 'small',
}
export enum LabelPosition {
Left = 'left',
Right = 'right',
}
const StyledContainer = styled.div<{ labelPosition?: LabelPosition }>`
${({ labelPosition }) =>
labelPosition === LabelPosition.Left
? `
flex-direction: row-reverse;
`
: `
flex-direction: row;
`};
align-items: center;
display: inline-flex;
`;
type RadioInputProps = {
'radio-size'?: RadioSize;
};
const StyledRadioInputBase = styled.input<RadioInputProps>`
-webkit-appearance: none;
appearance: none;
background-color: transparent;
border: 1px solid ${themeCssVariables.font.color.secondary};
border-radius: ${themeCssVariables.border.radius.rounded};
height: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '18px' : '16px'};
margin: 0;
margin-left: 3px;
position: relative;
width: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '18px' : '16px'};
:hover {
background-color: ${({ checked }) => {
if (!checked) {
return themeCssVariables.background.tertiary;
}
return '';
}};
outline: 4px solid
${({ checked }) => {
if (!checked) {
return themeCssVariables.background.tertiary;
}
return themeCssVariables.color.transparent.blue2;
}};
}
&:checked {
background-color: ${themeCssVariables.color.blue};
border: none;
&::after {
background-color: ${themeCssVariables.grayScale.gray1};
border-radius: 50%;
content: '';
height: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '8px' : '6px'};
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '8px' : '6px'};
}
}
&:disabled {
cursor: not-allowed;
opacity: 0.12;
}
`;
const StyledRadioInput = motion.create(StyledRadioInputBase);
type LabelProps = {
disabled?: boolean;
labelPosition?: LabelPosition;
};
const StyledLabel = styled.label<LabelProps>`
color: ${themeCssVariables.font.color.primary};
cursor: pointer;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.regular};
margin-left: ${({ labelPosition }) =>
labelPosition === LabelPosition.Right
? themeCssVariables.spacing[2]
: '0px'};
margin-right: ${({ labelPosition }) =>
labelPosition === LabelPosition.Left
? themeCssVariables.spacing[2]
: '0px'};
opacity: ${({ disabled }) => (disabled ? 0.32 : 1)};
`;
export type RadioProps = {
checked?: boolean;
className?: string;
name?: string;
disabled?: boolean;
label?: string;
labelPosition?: LabelPosition;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onCheckedChange?: (checked: boolean) => void;
size?: RadioSize;
style?: React.CSSProperties;
value?: string;
};
export const Radio = ({
checked,
className,
name = 'input-radio',
disabled = false,
label,
labelPosition = LabelPosition.Right,
onChange,
onCheckedChange,
size = RadioSize.Small,
value,
}: RadioProps) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(event);
onCheckedChange?.(event.target.checked);
};
const optionId = React.useId();
return (
<StyledContainer className={className} labelPosition={labelPosition}>
<StyledRadioInput
type="radio"
id={optionId}
name={name}
data-testid="input-radio"
tabIndex={-1}
checked={checked}
value={value || label}
radio-size={size}
disabled={disabled}
onChange={handleChange}
initial={{ scale: 0.95 }}
animate={{ scale: checked ? 1.05 : 0.95 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
/>
{label && (
<StyledLabel
htmlFor={optionId}
labelPosition={labelPosition}
disabled={disabled}
>
{label}
</StyledLabel>
)}
</StyledContainer>
);
};
Radio.Group = RadioGroup;
@@ -1,37 +0,0 @@
import React from 'react';
import { type RadioProps } from './Radio';
import { themeCssVariables } from '@ui/theme-constants';
type RadioGroupProps = React.PropsWithChildren & {
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onValueChange?: (value: string) => void;
};
export const RadioGroup = ({
value,
onChange,
onValueChange,
children,
}: RadioGroupProps) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(event);
onValueChange?.(event.target.value);
};
return (
<>
{React.Children.map(children, (child) => {
if (React.isValidElement<RadioProps>(child)) {
return React.cloneElement(child, {
style: { marginBottom: themeCssVariables.spacing[2] },
checked: child.props.value === value,
onChange: handleChange,
});
}
return child;
})}
</>
);
};
@@ -1,109 +0,0 @@
import { styled } from '@linaria/react';
import { IconFilter, IconSearch } from '@ui/display';
import { IconButton } from '@ui/input/button/components/IconButton';
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
import { type ChangeEvent, type ReactNode, useContext, useState } from 'react';
export type SearchInputProps = {
value: string;
onChange: (value: string) => void;
placeholder?: string;
filterDropdown?: (filterButton: ReactNode) => ReactNode;
autoFocus?: boolean;
disabled?: boolean;
className?: string;
};
const StyledWrapper = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
width: 100%;
`;
const StyledInputContainer = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.transparent.lighter};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
box-sizing: border-box;
display: flex;
flex: 1;
gap: ${themeCssVariables.spacing[1]};
height: 32px;
padding: 0 ${themeCssVariables.spacing[2]};
&:focus-within {
border-color: ${themeCssVariables.color.blue};
}
`;
const StyledIconContainer = styled.div<{
isFocused: boolean;
}>`
align-items: center;
color: ${({ isFocused }) =>
isFocused
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.light};
display: flex;
justify-content: center;
`;
const StyledInput = styled.input`
background: transparent;
border: none;
color: ${themeCssVariables.font.color.primary};
flex: 1;
font-family: ${themeCssVariables.font.family};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.regular};
outline: none;
width: 100%;
&::placeholder {
color: ${themeCssVariables.font.color.light};
font-weight: ${themeCssVariables.font.weight.medium};
}
&:disabled {
color: ${themeCssVariables.font.color.tertiary};
}
`;
export const SearchInput = ({
value,
onChange,
placeholder,
filterDropdown,
autoFocus,
disabled,
className,
}: SearchInputProps) => {
const { theme } = useContext(ThemeContext);
const [isFocused, setIsFocused] = useState(false);
const filterButton = <IconButton Icon={IconFilter} variant="secondary" />;
return (
<StyledWrapper className={className}>
<StyledInputContainer>
<StyledIconContainer isFocused={isFocused}>
<IconSearch size={theme.icon.size.md} />
</StyledIconContainer>
<StyledInput
value={value}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange(event.target.value)
}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={placeholder}
autoFocus={autoFocus}
disabled={disabled}
/>
</StyledInputContainer>
{filterDropdown && filterDropdown(filterButton)}
</StyledWrapper>
);
};
@@ -0,0 +1,64 @@
.root {
position: relative;
display: flex;
flex-shrink: 0;
align-items: center;
align-self: flex-start;
border-radius: 10px;
background-color: var(--t-background-transparent-medium);
cursor: pointer;
transition: background-color duration(normal) ease;
&[data-checked] {
background-color: var(--toggle-on-color, var(--t-color-blue));
}
&[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.centered {
align-self: center;
}
.small {
width: 24px;
height: 16px;
}
.medium {
width: 32px;
height: 20px;
}
.thumb {
position: absolute;
top: 50%;
left: 0;
display: block;
border-radius: 50%;
background-color: var(--t-background-primary);
transition: transform duration(normal) ease;
}
.small .thumb {
width: 12px;
height: 12px;
transform: translate(2px, -50%);
}
.medium .thumb {
width: 16px;
height: 16px;
transform: translate(2px, -50%);
}
.small[data-checked] .thumb {
transform: translate(10px, -50%);
}
.medium[data-checked] .thumb {
transform: translate(14px, -50%);
}
@@ -0,0 +1,8 @@
declare const classNames: {
readonly root: 'root';
readonly centered: 'centered';
readonly small: 'small';
readonly medium: 'medium';
readonly thumb: 'thumb';
};
export default classNames;
@@ -0,0 +1,50 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useState } from 'react';
import { ThemeProvider } from '@new-ui/theme-constants';
import { Toggle } from './Toggle';
const meta: Meta<typeof Toggle> = {
title: 'Input/Toggle',
component: Toggle,
args: { toggleSize: 'medium', 'aria-label': 'Example toggle' },
};
export default meta;
type Story = StoryObj<typeof Toggle>;
export const On: Story = { args: { value: true } };
export const Off: Story = { args: { value: false } };
export const Small: Story = { args: { value: true, toggleSize: 'small' } };
export const CustomColor: Story = {
args: { value: true, color: 'color(display-p3 0.2 0.7 0.4)' },
};
export const Disabled: Story = { args: { value: true, disabled: true } };
const ControlledToggle = () => {
const [value, setValue] = useState(false);
return (
<Toggle value={value} onChange={setValue} aria-label="Example toggle" />
);
};
export const Interactive: Story = {
render: () => <ControlledToggle />,
};
export const Dark: Story = {
args: { value: true },
decorators: [
(Story) => (
<ThemeProvider colorScheme="dark">
<Story />
</ThemeProvider>
),
],
};
@@ -1,52 +1,10 @@
import { styled } from '@linaria/react';
import { VisibilityHiddenInput } from '@ui/accessibility';
import { themeCssVariables } from '@ui/theme-constants';
import { motion } from 'framer-motion';
import { Switch } from '@base-ui/react/switch';
import { clsx } from 'clsx';
import styles from './Toggle.module.scss';
export type ToggleSize = 'small' | 'medium';
type ContainerProps = {
isOn: boolean;
color?: string;
toggleSize: ToggleSize;
centered?: boolean;
'data-disabled'?: boolean;
};
const StyledContainer = styled.label<ContainerProps>`
align-self: ${({ centered }) => (centered ? 'center' : 'flex-start')};
align-items: center;
background-color: ${({ isOn, color }) =>
isOn
? (color ?? themeCssVariables.color.blue)
: themeCssVariables.background.transparent.medium};
border-radius: 10px;
cursor: pointer;
display: flex;
flex-shrink: 0;
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
opacity: ${({ 'data-disabled': disabled }) => (disabled ? 0.5 : 1)};
pointer-events: ${({ 'data-disabled': disabled }) =>
disabled ? 'none' : 'auto'};
position: relative;
transition: background-color 0.3s ease;
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
`;
const StyledCircleBase = styled.span<{
size: ToggleSize;
}>`
background-color: ${themeCssVariables.background.primary};
border-radius: 50%;
display: block;
height: ${({ size }) => (size === 'small' ? 12 : 16)}px;
left: 0;
position: absolute;
width: ${({ size }) => (size === 'small' ? 12 : 16)}px;
`;
const StyledCircle = motion.create(StyledCircleBase);
export type ToggleProps = {
id?: string;
value?: boolean;
@@ -56,6 +14,8 @@ export type ToggleProps = {
className?: string;
centered?: boolean;
disabled?: boolean;
'aria-label'?: string;
'aria-labelledby'?: string;
};
export const Toggle = ({
@@ -67,37 +27,28 @@ export const Toggle = ({
className,
centered,
disabled,
}: ToggleProps) => {
const circleVariants = {
on: { x: toggleSize === 'small' ? 10 : 14 },
off: { x: 2 },
};
return (
<StyledContainer
isOn={value}
color={color}
toggleSize={toggleSize}
className={className}
centered={centered}
data-disabled={disabled}
>
<VisibilityHiddenInput
id={id}
type="checkbox"
checked={value}
disabled={disabled}
onChange={(event) => {
onChange?.(event.target.checked);
}}
/>
<StyledCircle
initial={false}
animate={value ? 'on' : 'off'}
variants={circleVariants}
size={toggleSize}
/>
</StyledContainer>
);
};
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
}: ToggleProps) => (
<Switch.Root
id={id}
checked={value}
disabled={disabled}
onCheckedChange={(checked) => onChange?.(checked)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
className={clsx(
styles.root,
styles[toggleSize],
centered && styles.centered,
className,
)}
style={
color
? ({ '--toggle-on-color': color } as React.CSSProperties)
: undefined
}
>
<Switch.Thumb className={styles.thumb} />
</Switch.Root>
);
@@ -1,97 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import {
Checkbox,
CheckboxAccent,
CheckboxShape,
CheckboxSize,
CheckboxVariant,
} from '../Checkbox';
const meta: Meta<typeof Checkbox> = {
title: 'UI/Input/Checkbox/Checkbox',
component: Checkbox,
};
export default meta;
type Story = StoryObj<typeof Checkbox>;
export const Default: Story = {
args: {
checked: false,
indeterminate: false,
hoverable: false,
disabled: false,
variant: CheckboxVariant.Primary,
size: CheckboxSize.Small,
shape: CheckboxShape.Squared,
accent: CheckboxAccent.Blue,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof Checkbox> = {
args: {},
argTypes: {
variant: { control: false },
size: { control: false },
indeterminate: { control: false },
checked: { control: false },
hoverable: { control: false },
shape: { control: false },
accent: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'state',
values: ['disabled', 'unchecked', 'checked', 'indeterminate'],
props: (state: string) => {
if (state === 'disabled') {
return { disabled: true };
}
if (state === 'checked') {
return { checked: true };
}
if (state === 'indeterminate') {
return { indeterminate: true };
}
return {};
},
},
{
name: 'shape',
values: Object.values(CheckboxShape),
props: (shape: CheckboxShape) => ({ shape }),
},
{
name: 'isHoverable',
values: ['default', 'hoverable'],
props: (isHoverable: string) => {
if (isHoverable === 'hoverable') {
return { hoverable: true };
}
return {};
},
},
{
name: 'size',
values: Object.values(CheckboxSize),
props: (size: CheckboxSize) => ({ size }),
},
{
name: 'accent',
values: Object.values(CheckboxAccent),
props: (accent: CheckboxAccent) => ({ accent }),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,64 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { LabelPosition, Radio, RadioSize } from '../Radio';
const meta: Meta<typeof Radio> = {
title: 'UI/Input/Radio/Radio',
component: Radio,
};
export default meta;
type Story = StoryObj<typeof Radio>;
export const Default: Story = {
args: {
label: 'Radio',
checked: false,
disabled: false,
size: RadioSize.Small,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof Radio> = {
args: {
label: 'Radio',
},
argTypes: {
size: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'checked',
values: [false, true],
props: (checked: boolean) => ({ checked }),
},
{
name: 'disabled',
values: [false, true],
props: (disabled: boolean) => ({ disabled }),
},
{
name: 'size',
values: Object.values(RadioSize),
props: (size: RadioSize) => ({ size }),
},
{
name: 'labelPosition',
values: Object.values(LabelPosition),
props: (labelPosition: LabelPosition) => ({
labelPosition,
}),
},
],
},
},
decorators: [CatalogDecorator],
};
@@ -1,54 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ComponentDecorator } from '@ui/testing';
import { useState } from 'react';
import { SearchInput } from '../SearchInput';
type InteractiveSearchInputProps = {
placeholder?: string;
disabled?: boolean;
autoFocus?: boolean;
className?: string;
};
const InteractiveSearchInput = ({
placeholder,
disabled,
autoFocus,
className,
}: InteractiveSearchInputProps) => {
const [value, setValue] = useState('');
return (
<SearchInput
value={value}
onChange={setValue}
placeholder={placeholder}
disabled={disabled}
autoFocus={autoFocus}
className={className}
/>
);
};
const meta: Meta<typeof InteractiveSearchInput> = {
title: 'UI/Input/SearchInput',
component: InteractiveSearchInput,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof InteractiveSearchInput>;
export const Default: Story = {
args: {
placeholder: 'Search...',
},
};
export const Disabled: Story = {
args: {
placeholder: 'Search...',
disabled: true,
},
};
@@ -1,98 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { themeCssVariables } from '@ui/theme-constants';
import { useState } from 'react';
import { Toggle, type ToggleSize } from '../Toggle';
type InteractiveToggleProps = {
initialValue?: boolean;
toggleSize?: ToggleSize;
color?: string;
disabled?: boolean;
};
const InteractiveToggle = ({
initialValue = false,
toggleSize = 'medium',
color,
disabled,
}: InteractiveToggleProps) => {
const [value, setValue] = useState(initialValue);
return (
<Toggle
value={value}
onChange={setValue}
toggleSize={toggleSize}
color={color}
disabled={disabled}
/>
);
};
const meta: Meta<typeof InteractiveToggle> = {
title: 'UI/Input/Toggle/Toggle',
component: InteractiveToggle,
};
export default meta;
type Story = StoryObj<typeof InteractiveToggle>;
export const Default: Story = {
args: {
initialValue: false,
disabled: false,
toggleSize: 'medium',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof InteractiveToggle> = {
args: {},
argTypes: {
toggleSize: { control: false },
initialValue: { control: false },
disabled: { control: false },
color: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'state',
values: ['disabled', 'off', 'on'],
props: (state: string) => {
if (state === 'disabled') {
return { disabled: true, initialValue: false };
}
if (state === 'on') {
return { initialValue: true };
}
return { initialValue: false };
},
},
{
name: 'size',
values: ['small', 'medium'] satisfies ToggleSize[],
props: (toggleSize: ToggleSize) => ({ toggleSize }),
},
{
name: 'color',
values: ['default', 'custom color'],
props: (color: string) => {
if (color === 'default') {
return {};
}
return { color: themeCssVariables.color.yellow };
},
},
],
},
},
decorators: [CatalogDecorator],
};
-97
View File
@@ -7,102 +7,5 @@
* |___/
*/
export type { AnimatedButtonProps } from './button/components/AnimatedButton';
export { AnimatedButton } from './button/components/AnimatedButton';
export type { AnimatedLightIconButtonProps } from './button/components/AnimatedLightIconButton';
export { AnimatedLightIconButton } from './button/components/AnimatedLightIconButton';
export type {
ButtonSize,
ButtonPosition,
ButtonVariant,
ButtonAccent,
ButtonProps,
} from './button/components/Button/Button';
export { Button } from './button/components/Button/Button';
export { baseTransitionTiming } from './button/components/Button/constant';
export type { ButtonGroupProps } from './button/components/ButtonGroup';
export { ButtonGroup } from './button/components/ButtonGroup';
export { ColorPickerButton } from './button/components/ColorPickerButton';
export type {
FloatingButtonSize,
FloatingButtonPosition,
FloatingButtonProps,
} from './button/components/FloatingButton';
export { FloatingButton } from './button/components/FloatingButton';
export type { FloatingButtonGroupProps } from './button/components/FloatingButtonGroup';
export { FloatingButtonGroup } from './button/components/FloatingButtonGroup';
export type {
FloatingIconButtonSize,
FloatingIconButtonPosition,
FloatingIconButtonProps,
} from './button/components/FloatingIconButton';
export { FloatingIconButton } from './button/components/FloatingIconButton';
export type { FloatingIconButtonGroupProps } from './button/components/FloatingIconButtonGroup';
export { FloatingIconButtonGroup } from './button/components/FloatingIconButtonGroup';
export type {
IconButtonSize,
IconButtonPosition,
IconButtonVariant,
IconButtonAccent,
IconButtonProps,
} from './button/components/IconButton';
export { IconButton } from './button/components/IconButton';
export type { IconButtonGroupProps } from './button/components/IconButtonGroup';
export { IconButtonGroup } from './button/components/IconButtonGroup';
export type { InsideButtonProps } from './button/components/InsideButton';
export { InsideButton } from './button/components/InsideButton';
export type {
LightButtonAccent,
LightButtonProps,
} from './button/components/LightButton';
export { LightButton } from './button/components/LightButton';
export type {
LightIconButtonAccent,
LightIconButtonSize,
LightIconButtonProps,
} from './button/components/LightIconButton';
export { LightIconButton } from './button/components/LightIconButton';
export type { LightIconButtonGroupProps } from './button/components/LightIconButtonGroup';
export { LightIconButtonGroup } from './button/components/LightIconButtonGroup';
export type { MainButtonVariant } from './button/components/MainButton';
export { MainButton } from './button/components/MainButton';
export type { RoundedIconButtonSize } from './button/components/RoundedIconButton';
export { RoundedIconButton } from './button/components/RoundedIconButton';
export {
StyledTabButton,
StyledTabContainer,
StyledTabHover,
} from './button/components/TabButton/internals/components/StyledTabBase';
export type { TabContentProps } from './button/components/TabButton/internals/components/TabContent';
export { TabContent } from './button/components/TabButton/internals/components/TabContent';
export { TabButton } from './button/components/TabButton/TabButton';
export { CodeEditor } from './code-editor/components/CodeEditor';
export type { CoreEditorHeaderProps } from './code-editor/components/CodeEditorHeader';
export { CoreEditorHeader } from './code-editor/components/CodeEditorHeader';
export { BASE_CODE_EDITOR_THEME_ID } from './code-editor/constants/BaseCodeEditorThemeId';
export { getBaseCodeEditorTheme } from './code-editor/theme/utils/getBaseCodeEditorTheme';
export type {
ColorSchemeSegmentProps,
ColorSchemeCardProps,
} from './color-scheme/components/ColorSchemeCard';
export { ColorSchemeCard } from './color-scheme/components/ColorSchemeCard';
export type { ColorSchemePickerProps } from './color-scheme/components/ColorSchemePicker';
export { ColorSchemePicker } from './color-scheme/components/ColorSchemePicker';
export { CardPicker } from './components/CardPicker';
export {
CheckboxVariant,
CheckboxShape,
CheckboxSize,
CheckboxAccent,
Checkbox,
} from './components/Checkbox';
export { IconListViewGrip } from './components/IconListViewGrip';
export type { RadioProps } from './components/Radio';
export { RadioSize, LabelPosition, Radio } from './components/Radio';
export { RadioGroup } from './components/RadioGroup';
export type { SearchInputProps } from './components/SearchInput';
export { SearchInput } from './components/SearchInput';
export type { ToggleSize, ToggleProps } from './components/Toggle';
export { Toggle } from './components/Toggle';
export type { ColorScheme } from './types/ColorScheme';
export type { SelectOption } from './types/SelectOption';
@@ -1 +0,0 @@
export type ColorScheme = 'Dark' | 'Light' | 'System';
@@ -1,15 +0,0 @@
import { type IconComponent } from '@ui/display';
import { type ThemeColor } from '@ui/theme';
export type SelectOption<
Value extends string | number | boolean | null = string,
> = {
Icon?: IconComponent | null;
iconThemeColor?: ThemeColor | null;
label: string;
fullLabel?: string;
value: Value;
disabled?: boolean;
color?: ThemeColor | 'transparent';
contextualText?: string;
};