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:
@@ -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>
|
||||
);
|
||||
};
|
||||
-92
@@ -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};
|
||||
}
|
||||
`;
|
||||
-48
@@ -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>
|
||||
);
|
||||
};
|
||||
-108
@@ -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],
|
||||
};
|
||||
-17
@@ -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],
|
||||
};
|
||||
-59
@@ -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],
|
||||
};
|
||||
-85
@@ -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],
|
||||
};
|
||||
-53
@@ -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],
|
||||
};
|
||||
-52
@@ -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],
|
||||
};
|
||||
-97
@@ -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 },
|
||||
};
|
||||
-30
@@ -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>
|
||||
);
|
||||
};
|
||||
-51
@@ -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],
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user