feat: wip import csv [part 1] (#1033)
* feat: wip import csv * feat: start implementing twenty UI * feat: new radio button component * feat: use new radio button component and fix scroll issue * fix: max height modal * feat: wip try to customize react-data-grid to match design * feat: wip match columns * feat: wip match column selection * feat: match column * feat: clean heading component & try to fix scroll in last step * feat: validation step * fix: small cleaning and remove unused component * feat: clean folder architecture * feat: remove translations * feat: remove chackra theme * feat: remove unused libraries * feat: use option button to open spreadsheet & fix stories * Fix lint and fix imports --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -38,7 +38,9 @@ type OwnProps = {
|
||||
const StyledColorSample = styled.div<{ colorName: string }>`
|
||||
background-color: ${({ theme, colorName }) =>
|
||||
theme.tag.background[colorName]};
|
||||
border: 1px solid ${({ theme, colorName }) => theme.color[colorName]};
|
||||
border: 1px solid
|
||||
${({ theme, colorName }) =>
|
||||
theme.color[colorName as keyof typeof theme.color]};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { TablerIconsProps } from '@tabler/icons-react';
|
||||
|
||||
import { SoonPill } from '@/ui/pill/components/SoonPill';
|
||||
import { rgba } from '@/ui/theme/constants/colors';
|
||||
@ -58,6 +59,8 @@ const StyledButton = styled.button<
|
||||
case 'primary':
|
||||
case 'secondary':
|
||||
return `${theme.background.transparent.medium}`;
|
||||
case 'danger':
|
||||
return `${theme.border.color.danger}`;
|
||||
case 'tertiary':
|
||||
default:
|
||||
return 'none';
|
||||
@ -80,6 +83,7 @@ const StyledButton = styled.button<
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
case 'secondary':
|
||||
case 'danger':
|
||||
return position === 'middle' ? `1px 0 1px 0` : `1px`;
|
||||
case 'tertiary':
|
||||
default:
|
||||
@ -98,10 +102,13 @@ const StyledButton = styled.button<
|
||||
|
||||
color: ${({ theme, variant, disabled }) => {
|
||||
if (disabled) {
|
||||
if (variant === 'primary') {
|
||||
return theme.color.gray0;
|
||||
} else {
|
||||
return theme.font.color.extraLight;
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return theme.color.gray0;
|
||||
case 'danger':
|
||||
return theme.border.color.danger;
|
||||
default:
|
||||
return theme.font.color.extraLight;
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,6 +163,8 @@ const StyledButton = styled.button<
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return `background: linear-gradient(0deg, ${theme.background.transparent.medium} 0%, ${theme.background.transparent.medium} 100%), ${theme.color.blue}`;
|
||||
case 'danger':
|
||||
return `background: ${theme.background.transparent.danger}`;
|
||||
default:
|
||||
return `background: ${theme.background.tertiary}`;
|
||||
}
|
||||
@ -178,7 +187,7 @@ const StyledButton = styled.button<
|
||||
`;
|
||||
|
||||
export function Button({
|
||||
icon,
|
||||
icon: initialIcon,
|
||||
title,
|
||||
fullWidth = false,
|
||||
variant = ButtonVariant.Primary,
|
||||
@ -188,6 +197,16 @@ export function Button({
|
||||
disabled = false,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const icon = useMemo(() => {
|
||||
if (!initialIcon || !React.isValidElement(initialIcon)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.cloneElement<TablerIconsProps>(initialIcon as any, {
|
||||
size: 14,
|
||||
});
|
||||
}, [initialIcon]);
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
fullWidth={fullWidth}
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export type CheckmarkProps = React.ComponentProps<typeof motion.path> & {
|
||||
isAnimating?: boolean;
|
||||
color?: string;
|
||||
duration?: number;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export function AnimatedCheckmark({
|
||||
isAnimating = false,
|
||||
color = '#FFF',
|
||||
duration = 0.5,
|
||||
size = 28,
|
||||
...restProps
|
||||
}: CheckmarkProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 52 52"
|
||||
width={size}
|
||||
height={size}
|
||||
>
|
||||
<motion.path
|
||||
{...restProps}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={4}
|
||||
d="M14 27l7.8 7.8L38 14"
|
||||
pathLength="1"
|
||||
strokeDasharray="1"
|
||||
strokeDashoffset={isAnimating ? '1' : '0'}
|
||||
animate={{ strokeDashoffset: isAnimating ? '0' : '1' }}
|
||||
transition={{ duration }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
125
front/src/modules/ui/dialog/components/Dialog.tsx
Normal file
125
front/src/modules/ui/dialog/components/Dialog.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { useCallback } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { Button, ButtonVariant } from '@/ui/button/components/Button';
|
||||
|
||||
const DialogOverlay = styled(motion.div)`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.overlay};
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
const DialogContainer = styled(motion.div)`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 320px;
|
||||
padding: 2em;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const DialogTitle = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(6)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const DialogMessage = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(6)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const DialogButton = styled(Button)`
|
||||
justify-content: center;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export type DialogButtonOptions = Omit<
|
||||
React.ComponentProps<typeof Button>,
|
||||
'fullWidth'
|
||||
>;
|
||||
|
||||
export type DialogProps = React.ComponentPropsWithoutRef<typeof motion.div> & {
|
||||
title?: string;
|
||||
message?: string;
|
||||
buttons?: DialogButtonOptions[];
|
||||
allowDismiss?: boolean;
|
||||
children?: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export function Dialog({
|
||||
title,
|
||||
message,
|
||||
buttons = [],
|
||||
allowDismiss = true,
|
||||
children,
|
||||
onClose,
|
||||
...rootProps
|
||||
}: DialogProps) {
|
||||
const closeSnackbar = useCallback(() => {
|
||||
onClose && onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const dialogVariants = {
|
||||
open: { opacity: 1 },
|
||||
closed: { opacity: 0 },
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
open: { y: 0 },
|
||||
closed: { y: '50vh' },
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
variants={dialogVariants}
|
||||
initial="closed"
|
||||
animate="open"
|
||||
exit="closed"
|
||||
onClick={(e) => {
|
||||
if (allowDismiss) {
|
||||
e.stopPropagation();
|
||||
closeSnackbar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContainer
|
||||
variants={containerVariants}
|
||||
transition={{ damping: 15, stiffness: 100 }}
|
||||
{...rootProps}
|
||||
>
|
||||
{title && <DialogTitle>{title}</DialogTitle>}
|
||||
{message && <DialogMessage>{message}</DialogMessage>}
|
||||
{children}
|
||||
{buttons.map((button) => (
|
||||
<DialogButton
|
||||
key={button.title}
|
||||
onClick={(e) => {
|
||||
button?.onClick?.(e);
|
||||
closeSnackbar();
|
||||
}}
|
||||
fullWidth={true}
|
||||
variant={button.variant ?? ButtonVariant.Secondary}
|
||||
{...button}
|
||||
/>
|
||||
))}
|
||||
</DialogContainer>
|
||||
</DialogOverlay>
|
||||
);
|
||||
}
|
||||
26
front/src/modules/ui/dialog/components/DialogProvider.tsx
Normal file
26
front/src/modules/ui/dialog/components/DialogProvider.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { dialogInternalState } from '../states/dialogState';
|
||||
|
||||
import { Dialog } from './Dialog';
|
||||
|
||||
export function DialogProvider({ children }: React.PropsWithChildren) {
|
||||
const [dialogState, setDialogState] = useRecoilState(dialogInternalState);
|
||||
|
||||
// Handle dialog close event
|
||||
const handleDialogClose = (id: string) => {
|
||||
setDialogState((prevState) => ({
|
||||
...prevState,
|
||||
queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{dialogState.queue.map((dialog) => (
|
||||
<Dialog {...dialog} onClose={() => handleDialogClose(dialog.id)} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
17
front/src/modules/ui/dialog/hooks/useDialog.ts
Normal file
17
front/src/modules/ui/dialog/hooks/useDialog.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DialogOptions, dialogSetQueueState } from '../states/dialogState';
|
||||
|
||||
export function useDialog() {
|
||||
const setDialogQueue = useSetRecoilState(dialogSetQueueState);
|
||||
|
||||
const enqueueDialog = (options?: Omit<DialogOptions, 'id'>) => {
|
||||
setDialogQueue({
|
||||
id: uuidv4(),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
return { enqueueDialog };
|
||||
}
|
||||
39
front/src/modules/ui/dialog/states/dialogState.ts
Normal file
39
front/src/modules/ui/dialog/states/dialogState.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { atom, selector } from 'recoil';
|
||||
|
||||
import { DialogProps } from '../components/Dialog';
|
||||
|
||||
export type DialogOptions = DialogProps & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type DialogState = {
|
||||
maxQueue: number;
|
||||
queue: DialogOptions[];
|
||||
};
|
||||
|
||||
export const dialogInternalState = atom<DialogState>({
|
||||
key: 'dialog/internal-state',
|
||||
default: {
|
||||
maxQueue: 2,
|
||||
queue: [],
|
||||
},
|
||||
});
|
||||
|
||||
export const dialogSetQueueState = selector<DialogOptions | null>({
|
||||
key: 'dialog/queue-state',
|
||||
get: ({ get: _get }) => null, // We don't care about getting the value
|
||||
set: ({ set }, newValue) =>
|
||||
set(dialogInternalState, (prev) => {
|
||||
if (prev.queue.length >= prev.maxQueue) {
|
||||
return {
|
||||
...prev,
|
||||
queue: [...prev.queue.slice(1), newValue] as DialogOptions[],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
queue: [...prev.queue, newValue] as DialogOptions[],
|
||||
};
|
||||
}),
|
||||
});
|
||||
@ -7,13 +7,15 @@ import { hoverBackground } from '@/ui/theme/constants/effects';
|
||||
|
||||
import { DropdownMenuItem } from './DropdownMenuItem';
|
||||
|
||||
type Props = {
|
||||
type Props = React.ComponentProps<'li'> & {
|
||||
selected?: boolean;
|
||||
onClick: () => void;
|
||||
hovered?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const DropdownMenuSelectableItemContainer = styled(DropdownMenuItem)<Props>`
|
||||
const DropdownMenuSelectableItemContainer = styled(DropdownMenuItem)<
|
||||
Pick<Props, 'hovered'>
|
||||
>`
|
||||
${hoverBackground};
|
||||
|
||||
align-items: center;
|
||||
@ -27,12 +29,15 @@ const DropdownMenuSelectableItemContainer = styled(DropdownMenuItem)<Props>`
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(2)});
|
||||
`;
|
||||
|
||||
const StyledLeftContainer = styled.div`
|
||||
const StyledLeftContainer = styled.div<Pick<Props, 'disabled'>>`
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
|
||||
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
@ -45,9 +50,19 @@ export function DropdownMenuSelectableItem({
|
||||
onClick,
|
||||
children,
|
||||
hovered,
|
||||
disabled,
|
||||
...restProps
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const theme = useTheme();
|
||||
|
||||
function handleClick(event: React.MouseEvent<HTMLLIElement>) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(event);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (hovered) {
|
||||
window.scrollTo({
|
||||
@ -58,12 +73,12 @@ export function DropdownMenuSelectableItem({
|
||||
|
||||
return (
|
||||
<DropdownMenuSelectableItemContainer
|
||||
onClick={onClick}
|
||||
selected={selected}
|
||||
{...restProps}
|
||||
onClick={handleClick}
|
||||
hovered={hovered}
|
||||
data-testid="dropdown-menu-item"
|
||||
>
|
||||
<StyledLeftContainer>{children}</StyledLeftContainer>
|
||||
<StyledLeftContainer disabled={disabled}>{children}</StyledLeftContainer>
|
||||
<StyledRightIcon>
|
||||
{selected && <IconCheck size={theme.icon.size.md} />}
|
||||
</StyledRightIcon>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
export { IconAddressBook } from './components/IconAddressBook';
|
||||
export {
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconArchive,
|
||||
IconArrowBack,
|
||||
IconArrowNarrowDown,
|
||||
IconArrowNarrowUp,
|
||||
IconArrowRight,
|
||||
@ -26,10 +26,13 @@ export {
|
||||
IconColorSwatch,
|
||||
IconMessageCircle as IconComment,
|
||||
IconCopy,
|
||||
IconCross,
|
||||
IconCurrencyDollar,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconFileImport,
|
||||
IconFileUpload,
|
||||
IconForbid,
|
||||
IconHeart,
|
||||
IconHelpCircle,
|
||||
IconInbox,
|
||||
|
||||
@ -6,6 +6,7 @@ import { IconCheck, IconMinus } from '@/ui/icon';
|
||||
export enum CheckboxVariant {
|
||||
Primary = 'primary',
|
||||
Secondary = 'secondary',
|
||||
Tertiary = 'tertiary',
|
||||
}
|
||||
|
||||
export enum CheckboxShape {
|
||||
@ -21,7 +22,8 @@ export enum CheckboxSize {
|
||||
type OwnProps = {
|
||||
checked: boolean;
|
||||
indeterminate?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onCheckedChange?: (value: boolean) => void;
|
||||
variant?: CheckboxVariant;
|
||||
size?: CheckboxSize;
|
||||
shape?: CheckboxShape;
|
||||
@ -33,13 +35,15 @@ const StyledInputContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<{
|
||||
type InputProps = {
|
||||
checkboxSize: CheckboxSize;
|
||||
variant: CheckboxVariant;
|
||||
indeterminate?: boolean;
|
||||
shape?: CheckboxShape;
|
||||
isChecked: boolean;
|
||||
}>`
|
||||
isChecked?: boolean;
|
||||
};
|
||||
|
||||
const StyledInput = styled.input<InputProps>`
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
@ -61,18 +65,25 @@ const StyledInput = styled.input<{
|
||||
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
|
||||
background: ${({ theme, indeterminate, isChecked }) =>
|
||||
indeterminate || isChecked ? theme.color.blue : 'transparent'};
|
||||
border-color: ${({ theme, indeterminate, isChecked, variant }) =>
|
||||
indeterminate || isChecked
|
||||
? theme.color.blue
|
||||
: variant === CheckboxVariant.Primary
|
||||
? theme.border.color.inverted
|
||||
: theme.border.color.secondaryInverted};
|
||||
border-color: ${({ theme, indeterminate, isChecked, variant }) => {
|
||||
switch (true) {
|
||||
case indeterminate || isChecked:
|
||||
return theme.color.blue;
|
||||
case variant === CheckboxVariant.Primary:
|
||||
return theme.border.color.inverted;
|
||||
case variant === CheckboxVariant.Tertiary:
|
||||
return theme.border.color.medium;
|
||||
default:
|
||||
return theme.border.color.secondaryInverted;
|
||||
}
|
||||
}};
|
||||
border-radius: ${({ theme, shape }) =>
|
||||
shape === CheckboxShape.Rounded
|
||||
? theme.border.radius.rounded
|
||||
: theme.border.radius.sm};
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-width: ${({ variant }) =>
|
||||
variant === CheckboxVariant.Tertiary ? '2px' : '1px'};
|
||||
content: '';
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@ -81,8 +92,11 @@ const StyledInput = styled.input<{
|
||||
}
|
||||
|
||||
& + label > svg {
|
||||
--padding: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '2px' : '1px'};
|
||||
--padding: ${({ checkboxSize, variant }) =>
|
||||
checkboxSize === CheckboxSize.Large ||
|
||||
variant === CheckboxVariant.Tertiary
|
||||
? '2px'
|
||||
: '1px'};
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '16px' : '12px'};
|
||||
height: var(--size);
|
||||
@ -97,6 +111,7 @@ const StyledInput = styled.input<{
|
||||
export function Checkbox({
|
||||
checked,
|
||||
onChange,
|
||||
onCheckedChange,
|
||||
indeterminate,
|
||||
variant = CheckboxVariant.Primary,
|
||||
size = CheckboxSize.Small,
|
||||
@ -108,9 +123,11 @@ export function Checkbox({
|
||||
React.useEffect(() => {
|
||||
setIsInternalChecked(checked);
|
||||
}, [checked]);
|
||||
function handleChange(value: boolean) {
|
||||
onChange?.(value);
|
||||
setIsInternalChecked(!isInternalChecked);
|
||||
|
||||
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
onChange?.(event);
|
||||
onCheckedChange?.(event.target.checked);
|
||||
setIsInternalChecked(event.target.checked);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -126,7 +143,7 @@ export function Checkbox({
|
||||
checkboxSize={size}
|
||||
shape={shape}
|
||||
isChecked={isInternalChecked}
|
||||
onChange={(event) => handleChange(event.target.checked)}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label htmlFor="checkbox">
|
||||
{indeterminate ? (
|
||||
|
||||
157
front/src/modules/ui/input/radio/components/Radio.tsx
Normal file
157
front/src/modules/ui/input/radio/components/Radio.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import * as React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { rgba } from '@/ui/theme/constants/colors';
|
||||
|
||||
import { RadioGroup } from './RadioGroup';
|
||||
|
||||
export enum RadioSize {
|
||||
Large = 'large',
|
||||
Small = 'small',
|
||||
}
|
||||
|
||||
export enum LabelPosition {
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
}
|
||||
|
||||
const Container = styled.div<{ labelPosition?: LabelPosition }>`
|
||||
${({ labelPosition }) =>
|
||||
labelPosition === LabelPosition.Left
|
||||
? `
|
||||
flex-direction: row-reverse;
|
||||
`
|
||||
: `
|
||||
flex-direction: row;
|
||||
`};
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
type RadioInputProps = {
|
||||
radioSize?: RadioSize;
|
||||
};
|
||||
|
||||
const RadioInput = styled(motion.input)<RadioInputProps>`
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid ${({ theme }) => theme.font.color.secondary};
|
||||
border-radius: 50%;
|
||||
:hover {
|
||||
background-color: ${({ theme, checked }) => {
|
||||
if (!checked) {
|
||||
return theme.background.tertiary;
|
||||
}
|
||||
}};
|
||||
outline: 4px solid
|
||||
${({ theme, checked }) => {
|
||||
if (!checked) {
|
||||
return theme.background.tertiary;
|
||||
}
|
||||
return rgba(theme.color.blue, 0.12);
|
||||
}};
|
||||
}
|
||||
&:checked {
|
||||
background-color: ${({ theme }) => theme.color.blue};
|
||||
border: none;
|
||||
&::after {
|
||||
background-color: ${({ theme }) => theme.color.gray0};
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
height: ${({ radioSize }) =>
|
||||
radioSize === RadioSize.Large ? '8px' : '6px'};
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: ${({ radioSize }) =>
|
||||
radioSize === RadioSize.Large ? '8px' : '6px'};
|
||||
}
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.12;
|
||||
}
|
||||
height: ${({ radioSize }) =>
|
||||
radioSize === RadioSize.Large ? '18px' : '16px'};
|
||||
position: relative;
|
||||
width: ${({ radioSize }) =>
|
||||
radioSize === RadioSize.Large ? '18px' : '16px'};
|
||||
`;
|
||||
|
||||
type LabelProps = {
|
||||
disabled?: boolean;
|
||||
labelPosition?: LabelPosition;
|
||||
};
|
||||
|
||||
const Label = styled.label<LabelProps>`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
cursor: pointer;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin-left: ${({ theme, labelPosition }) =>
|
||||
labelPosition === LabelPosition.Right ? theme.spacing(2) : '0px'};
|
||||
margin-right: ${({ theme, labelPosition }) =>
|
||||
labelPosition === LabelPosition.Left ? theme.spacing(2) : '0px'};
|
||||
opacity: ${({ disabled }) => (disabled ? 0.32 : 1)};
|
||||
`;
|
||||
|
||||
export type RadioProps = {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
checked?: boolean;
|
||||
value?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
size?: RadioSize;
|
||||
disabled?: boolean;
|
||||
labelPosition?: LabelPosition;
|
||||
};
|
||||
|
||||
export function Radio({
|
||||
checked,
|
||||
value,
|
||||
onChange,
|
||||
onCheckedChange,
|
||||
size = RadioSize.Small,
|
||||
labelPosition = LabelPosition.Right,
|
||||
disabled = false,
|
||||
...restProps
|
||||
}: RadioProps) {
|
||||
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
onChange?.(event);
|
||||
onCheckedChange?.(event.target.checked);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container {...restProps} labelPosition={labelPosition}>
|
||||
<RadioInput
|
||||
type="radio"
|
||||
id="input-radio"
|
||||
name="input-radio"
|
||||
data-testid="input-radio"
|
||||
checked={checked}
|
||||
value={value}
|
||||
radioSize={size}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
initial={{ scale: 0.95 }}
|
||||
animate={{ scale: checked ? 1.05 : 0.95 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
/>
|
||||
{value && (
|
||||
<Label
|
||||
htmlFor="input-radio"
|
||||
labelPosition={labelPosition}
|
||||
disabled={disabled}
|
||||
>
|
||||
{value}
|
||||
</Label>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Radio.Group = RadioGroup;
|
||||
39
front/src/modules/ui/input/radio/components/RadioGroup.tsx
Normal file
39
front/src/modules/ui/input/radio/components/RadioGroup.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { RadioProps } from './Radio';
|
||||
|
||||
type RadioGroupProps = React.PropsWithChildren & {
|
||||
value?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onValueChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export function RadioGroup({
|
||||
value,
|
||||
onChange,
|
||||
onValueChange,
|
||||
children,
|
||||
}: RadioGroupProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
function 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: theme.spacing(2) },
|
||||
checked: child.props.value === value,
|
||||
onChange: handleChange,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { LabelPosition, Radio, RadioSize } from '../Radio';
|
||||
|
||||
const meta: Meta<typeof Radio> = {
|
||||
title: 'UI/Input/Radio',
|
||||
component: Radio,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Radio>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: 'Radio',
|
||||
checked: false,
|
||||
disabled: false,
|
||||
size: RadioSize.Small,
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: Story = {
|
||||
args: {
|
||||
value: 'Radio',
|
||||
},
|
||||
argTypes: {
|
||||
value: { control: false },
|
||||
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: string) => ({
|
||||
labelPosition,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
import {
|
||||
ChangeEvent,
|
||||
FocusEventHandler,
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
InputHTMLAttributes,
|
||||
useRef,
|
||||
useState,
|
||||
@ -13,6 +15,7 @@ import { IconAlertCircle } from '@/ui/icon';
|
||||
import { IconEye, IconEyeOff } from '@/ui/icon/index';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||
|
||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||
|
||||
@ -95,22 +98,26 @@ const StyledTrailingIcon = styled.div`
|
||||
|
||||
const INPUT_TYPE_PASSWORD = 'password';
|
||||
|
||||
export function TextInput({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
fullWidth,
|
||||
error,
|
||||
required,
|
||||
type,
|
||||
disableHotkeys = false,
|
||||
...props
|
||||
}: OwnProps): JSX.Element {
|
||||
function TextInputComponent(
|
||||
{
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
fullWidth,
|
||||
error,
|
||||
required,
|
||||
type,
|
||||
disableHotkeys = false,
|
||||
...props
|
||||
}: OwnProps,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
): JSX.Element {
|
||||
const theme = useTheme();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const combinedRef = useCombinedRefs(ref, inputRef);
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
@ -151,7 +158,7 @@ export function TextInput({
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
autoComplete="off"
|
||||
ref={inputRef}
|
||||
ref={combinedRef}
|
||||
tabIndex={props.tabIndex ?? 0}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@ -189,3 +196,5 @@ export function TextInput({
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export const TextInput = forwardRef(TextInputComponent);
|
||||
|
||||
@ -7,20 +7,42 @@ import {
|
||||
useListenClickOutside,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
const ModalDiv = styled(motion.div)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(10)};
|
||||
width: calc(400px - ${({ theme }) => theme.spacing(10 * 2)});
|
||||
`;
|
||||
|
||||
const ModalDiv = styled(motion.div)`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
overflow: hidden;
|
||||
max-height: 90vh;
|
||||
z-index: 10000; // should be higher than Backdrop's z-index
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
padding: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: ${({ theme }) => theme.spacing(10)};
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
padding: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const BackDrop = styled(motion.div)`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.overlay};
|
||||
@ -34,7 +56,31 @@ const BackDrop = styled(motion.div)`
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
type Props = React.PropsWithChildren &
|
||||
/**
|
||||
* Modal components
|
||||
*/
|
||||
type ModalHeaderProps = React.PropsWithChildren & React.ComponentProps<'div'>;
|
||||
|
||||
function ModalHeader({ children, ...restProps }: ModalHeaderProps) {
|
||||
return <StyledHeader {...restProps}>{children}</StyledHeader>;
|
||||
}
|
||||
|
||||
type ModalContentProps = React.PropsWithChildren & React.ComponentProps<'div'>;
|
||||
|
||||
function ModalContent({ children, ...restProps }: ModalContentProps) {
|
||||
return <StyledContent {...restProps}>{children}</StyledContent>;
|
||||
}
|
||||
|
||||
type ModalFooterProps = React.PropsWithChildren & React.ComponentProps<'div'>;
|
||||
|
||||
function ModalFooter({ children, ...restProps }: ModalFooterProps) {
|
||||
return <StyledFooter {...restProps}>{children}</StyledFooter>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal
|
||||
*/
|
||||
type ModalProps = React.PropsWithChildren &
|
||||
React.ComponentProps<'div'> & {
|
||||
isOpen?: boolean;
|
||||
onOutsideClick?: () => void;
|
||||
@ -51,8 +97,8 @@ export function Modal({
|
||||
children,
|
||||
onOutsideClick,
|
||||
...restProps
|
||||
}: Props) {
|
||||
const modalRef = useRef(null);
|
||||
}: ModalProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [modalRef],
|
||||
@ -67,15 +113,23 @@ export function Modal({
|
||||
return (
|
||||
<BackDrop>
|
||||
<ModalDiv
|
||||
layout
|
||||
// framer-motion seems to have typing problems with refs
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
ref={modalRef}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
layout
|
||||
variants={modalVariants}
|
||||
ref={modalRef}
|
||||
{...restProps}
|
||||
>
|
||||
<StyledContainer {...restProps}>{children}</StyledContainer>
|
||||
{children}
|
||||
</ModalDiv>
|
||||
</BackDrop>
|
||||
);
|
||||
}
|
||||
|
||||
Modal.Header = ModalHeader;
|
||||
Modal.Content = ModalContent;
|
||||
Modal.Footer = ModalFooter;
|
||||
|
||||
@ -22,7 +22,11 @@ const StyledContentContainer = styled.div`
|
||||
|
||||
const args = {
|
||||
isOpen: true,
|
||||
children: <StyledContentContainer>Lorem ipsum</StyledContentContainer>,
|
||||
children: (
|
||||
<Modal.Content>
|
||||
<StyledContentContainer>Lorem ipsum</StyledContentContainer>
|
||||
</Modal.Content>
|
||||
),
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { motion, useAnimation } from 'framer-motion';
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
barWidth?: number;
|
||||
barColor?: string;
|
||||
}
|
||||
|
||||
export function CircularProgressBar({
|
||||
size = 50,
|
||||
barWidth = 5,
|
||||
barColor = 'currentColor',
|
||||
}: Props) {
|
||||
const controls = useAnimation();
|
||||
|
||||
const circumference = useMemo(
|
||||
() => 2 * Math.PI * (size / 2 - barWidth),
|
||||
[size, barWidth],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function animateIndeterminate() {
|
||||
const baseSegment = Math.max(5, circumference / 10); // Adjusting for smaller values
|
||||
|
||||
// Adjusted sequence based on baseSegment
|
||||
const dashSequences = [
|
||||
`${baseSegment} ${circumference - baseSegment}`,
|
||||
`${baseSegment * 2} ${circumference - baseSegment * 2}`,
|
||||
`${baseSegment * 3} ${circumference - baseSegment * 3}`,
|
||||
`${baseSegment * 2} ${circumference - baseSegment * 2}`,
|
||||
`${baseSegment} ${circumference - baseSegment}`,
|
||||
];
|
||||
|
||||
await controls.start({
|
||||
strokeDasharray: dashSequences,
|
||||
rotate: [0, 720],
|
||||
transition: {
|
||||
strokeDasharray: {
|
||||
duration: 2,
|
||||
ease: 'linear',
|
||||
repeat: Infinity,
|
||||
repeatType: 'loop',
|
||||
},
|
||||
rotate: {
|
||||
duration: 2,
|
||||
ease: 'linear',
|
||||
repeat: Infinity,
|
||||
repeatType: 'loop',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
animateIndeterminate();
|
||||
}, [circumference, controls]);
|
||||
|
||||
return (
|
||||
<motion.svg width={size} height={size} animate={controls}>
|
||||
<motion.circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={size / 2 - barWidth}
|
||||
fill="none"
|
||||
stroke={barColor}
|
||||
strokeWidth={barWidth}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { CircularProgressBar } from '../CircularProgressBar';
|
||||
|
||||
const meta: Meta<typeof CircularProgressBar> = {
|
||||
title: 'UI/CircularProgressBar/CircularProgressBar',
|
||||
component: CircularProgressBar,
|
||||
args: {
|
||||
size: 50,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof CircularProgressBar>;
|
||||
const args = {};
|
||||
const defaultArgTypes = {
|
||||
control: false,
|
||||
};
|
||||
export const Default: Story = {
|
||||
args,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog = {
|
||||
args: {
|
||||
...args,
|
||||
},
|
||||
argTypes: {
|
||||
strokeWidth: defaultArgTypes,
|
||||
segmentColor: defaultArgTypes,
|
||||
},
|
||||
parameters: {
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'barColor',
|
||||
values: [undefined, 'red'],
|
||||
props: (barColor: string) => ({ barColor }),
|
||||
labels: (color: string) => `Segment Color: ${color ?? 'default'}`,
|
||||
},
|
||||
{
|
||||
name: 'barWidth',
|
||||
values: [undefined, 5, 10],
|
||||
props: (barWidth: number) => ({ barWidth }),
|
||||
labels: (width: number) =>
|
||||
`Stroke Width: ${width ? width + ' px' : 'default'}`,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
values: [undefined, 80, 30],
|
||||
props: (size: number) => ({ size }),
|
||||
labels: (size: number) => `Size: ${size ? size + ' px' : 'default'}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
117
front/src/modules/ui/step-bar/components/Step.tsx
Normal file
117
front/src/modules/ui/step-bar/components/Step.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { AnimatedCheckmark } from '@/ui/checkmark/components/AnimatedCheckmark';
|
||||
|
||||
const Container = styled.div<{ isLast: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-grow: ${({ isLast }) => (isLast ? '0' : '1')};
|
||||
`;
|
||||
|
||||
const StepCircle = styled(motion.div)<{ isCurrent: boolean }>`
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
flex-basis: auto;
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
const StepIndex = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StepLabel = styled.span<{ isActive: boolean }>`
|
||||
color: ${({ theme, isActive }) =>
|
||||
isActive ? theme.font.color.primary : theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StepLine = styled(motion.div)<{ isActive: boolean }>`
|
||||
height: 2px;
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type StepProps = React.PropsWithChildren &
|
||||
React.ComponentProps<'div'> & {
|
||||
isActive?: boolean;
|
||||
isLast?: boolean;
|
||||
index?: number;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const Step = ({
|
||||
isActive = false,
|
||||
isLast = false,
|
||||
index = 0,
|
||||
label,
|
||||
children,
|
||||
}: StepProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const variantsCircle = {
|
||||
active: {
|
||||
backgroundColor: theme.font.color.primary,
|
||||
borderColor: theme.font.color.primary,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
inactive: {
|
||||
backgroundColor: theme.background.transparent.lighter,
|
||||
borderColor: theme.border.color.medium,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
const variantsLine = {
|
||||
active: {
|
||||
backgroundColor: theme.font.color.primary,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
inactive: {
|
||||
backgroundColor: theme.border.color.medium,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Container isLast={isLast}>
|
||||
<StepCircle
|
||||
isCurrent={isActive}
|
||||
variants={variantsCircle}
|
||||
animate={isActive ? 'active' : 'inactive'}
|
||||
>
|
||||
{isActive && (
|
||||
<AnimatedCheckmark isAnimating={isActive} color={theme.color.gray0} />
|
||||
)}
|
||||
{!isActive && <StepIndex>{index + 1}</StepIndex>}
|
||||
</StepCircle>
|
||||
<StepLabel isActive={isActive}>{label}</StepLabel>
|
||||
{!isLast && (
|
||||
<StepLine
|
||||
isActive={isActive}
|
||||
variants={variantsLine}
|
||||
animate={isActive ? 'active' : 'inactive'}
|
||||
/>
|
||||
)}
|
||||
{isActive && children}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Step.displayName = 'StepBar/Step';
|
||||
42
front/src/modules/ui/step-bar/components/StepBar.tsx
Normal file
42
front/src/modules/ui/step-bar/components/StepBar.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Step, StepProps } from './Step';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export type StepsProps = React.PropsWithChildren &
|
||||
React.ComponentProps<'div'> & {
|
||||
activeStep: number;
|
||||
};
|
||||
|
||||
export const StepBar = ({ children, activeStep, ...restProps }: StepsProps) => {
|
||||
return (
|
||||
<Container {...restProps}>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (!React.isValidElement(child)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the child is not a Step, return it as-is
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (child.type?.displayName !== Step.displayName) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return React.cloneElement<StepProps>(child as any, {
|
||||
index,
|
||||
isActive: index <= activeStep,
|
||||
isLast: index === React.Children.count(children) - 1,
|
||||
});
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
StepBar.Step = Step;
|
||||
59
front/src/modules/ui/step-bar/hooks/useStepBar.ts
Normal file
59
front/src/modules/ui/step-bar/hooks/useStepBar.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { stepBarInternalState } from '../states/stepBarInternalState';
|
||||
|
||||
export type StepsOptions = {
|
||||
initialStep: number;
|
||||
};
|
||||
|
||||
export function useStepBar({ initialStep }: StepsOptions) {
|
||||
const [stepsState, setStepsState] = useRecoilState(stepBarInternalState);
|
||||
|
||||
function nextStep() {
|
||||
setStepsState((prevState) => ({
|
||||
...prevState,
|
||||
activeStep: prevState.activeStep + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function prevStep() {
|
||||
setStepsState((prevState) => ({
|
||||
...prevState,
|
||||
activeStep: prevState.activeStep - 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setStepsState((prevState) => ({
|
||||
...prevState,
|
||||
activeStep: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
const setStep = useCallback(
|
||||
(step: number) => {
|
||||
setStepsState((prevState) => ({
|
||||
...prevState,
|
||||
activeStep: step,
|
||||
}));
|
||||
},
|
||||
[setStepsState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialStep !== undefined) {
|
||||
setStep(initialStep);
|
||||
}
|
||||
// We only want this to happen on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return {
|
||||
nextStep,
|
||||
prevStep,
|
||||
reset,
|
||||
setStep,
|
||||
activeStep: stepsState.activeStep,
|
||||
};
|
||||
}
|
||||
12
front/src/modules/ui/step-bar/states/stepBarInternalState.ts
Normal file
12
front/src/modules/ui/step-bar/states/stepBarInternalState.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export type StepsState = {
|
||||
activeStep: number;
|
||||
};
|
||||
|
||||
export const stepBarInternalState = atom<StepsState>({
|
||||
key: 'step-bar/internal-state',
|
||||
default: {
|
||||
activeStep: -1,
|
||||
},
|
||||
});
|
||||
@ -9,6 +9,7 @@ import { useTheme } from '@emotion/react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||
@ -20,7 +21,13 @@ import type {
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
|
||||
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
|
||||
import {
|
||||
IconChevronLeft,
|
||||
IconFileImport,
|
||||
IconMinus,
|
||||
IconPlus,
|
||||
IconTag,
|
||||
} from '@/ui/icon';
|
||||
import {
|
||||
hiddenTableColumnsState,
|
||||
tableColumnsState,
|
||||
@ -58,6 +65,8 @@ export const TableOptionsDropdownButton = ({
|
||||
}: TableOptionsDropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { openSpreadsheetImport } = useSpreadsheetImport();
|
||||
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
|
||||
undefined,
|
||||
@ -85,6 +94,16 @@ export const TableOptionsDropdownButton = ({
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
function handleImport() {
|
||||
openSpreadsheetImport({
|
||||
onSubmit: (datam, file) => {
|
||||
console.log('datam', datam);
|
||||
console.log('file', file);
|
||||
},
|
||||
fields: [],
|
||||
});
|
||||
}
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(columnId: string, nextIsVisible: boolean) => {
|
||||
const nextColumns = columns.map((column) =>
|
||||
@ -226,6 +245,12 @@ export const TableOptionsDropdownButton = ({
|
||||
<IconTag size={theme.icon.size.md} />
|
||||
Properties
|
||||
</DropdownMenuItem>
|
||||
{false && (
|
||||
<DropdownMenuItem onClick={handleImport}>
|
||||
<IconFileImport size={theme.icon.size.md} />
|
||||
Import
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import DarkNoise from '../assets/dark-noise.jpg';
|
||||
import LightNoise from '../assets/light-noise.png';
|
||||
|
||||
import { grayScale, rgba } from './colors';
|
||||
import { color, grayScale, rgba } from './colors';
|
||||
|
||||
export const backgroundLight = {
|
||||
noisy: `url(${LightNoise.toString()});`,
|
||||
@ -16,6 +16,7 @@ export const backgroundLight = {
|
||||
medium: rgba(grayScale.gray100, 0.08),
|
||||
light: rgba(grayScale.gray100, 0.04),
|
||||
lighter: rgba(grayScale.gray100, 0.02),
|
||||
danger: rgba(color.red, 0.08),
|
||||
},
|
||||
overlay: rgba(grayScale.gray80, 0.8),
|
||||
};
|
||||
@ -33,6 +34,7 @@ export const backgroundDark = {
|
||||
medium: rgba(grayScale.gray0, 0.1),
|
||||
light: rgba(grayScale.gray0, 0.06),
|
||||
lighter: rgba(grayScale.gray0, 0.03),
|
||||
danger: rgba(color.red, 0.08),
|
||||
},
|
||||
overlay: rgba(grayScale.gray80, 0.8),
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { grayScale } from './colors';
|
||||
import { color, grayScale, rgba } from './colors';
|
||||
|
||||
const common = {
|
||||
radius: {
|
||||
@ -16,6 +16,7 @@ export const borderLight = {
|
||||
light: grayScale.gray15,
|
||||
secondaryInverted: grayScale.gray50,
|
||||
inverted: grayScale.gray60,
|
||||
danger: rgba(color.red, 0.14),
|
||||
},
|
||||
...common,
|
||||
};
|
||||
@ -27,6 +28,7 @@ export const borderDark = {
|
||||
light: grayScale.gray70,
|
||||
secondaryInverted: grayScale.gray35,
|
||||
inverted: grayScale.gray20,
|
||||
danger: rgba(color.red, 0.14),
|
||||
},
|
||||
...common,
|
||||
};
|
||||
|
||||
@ -22,7 +22,7 @@ export const grayScale = {
|
||||
gray0: '#ffffff',
|
||||
};
|
||||
|
||||
export const color: { [key: string]: string } = {
|
||||
export const color = {
|
||||
yellow: '#ffd338',
|
||||
yellow80: '#2e2a1a',
|
||||
yellow70: '#453d1e',
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { rgba } from '../theme/constants/colors';
|
||||
|
||||
export enum TooltipPosition {
|
||||
Top = 'top',
|
||||
Left = 'left',
|
||||
@ -9,16 +11,21 @@ export enum TooltipPosition {
|
||||
}
|
||||
|
||||
export const AppTooltip = styled(Tooltip)`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
||||
background-color: ${({ theme }) => rgba(theme.color.gray80, 0.8)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
color: ${({ theme }) => theme.color.gray0};
|
||||
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
|
||||
max-width: 40%;
|
||||
overflow: visible;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
word-break: break-word;
|
||||
|
||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||
|
||||
Reference in New Issue
Block a user