removed @chakra-ui dependencies (#7004)
Issue #6976 @FelixMalfait I could not do ``` import { Banner } from 'twenty-ui'; const StyledBanner = styled(Banner) display: flex; align-items: center; padding: ${({ theme }) => theme.spacing(8)}; position: absolute; border-radius: 8px; &:hover { background-color: ${({ theme }) => theme.accent.primary}; } ; ``` The styles wont get overridden for Banner, so for now I styled a new banner in `UnmatchColumnBanner` which is inconsistent. I couldnt figure out why css properties are not being overridden, need help! @Bonapara Question - Should the click work on entire banner or just cheveron? For now it just on cheveron click. https://github.com/user-attachments/assets/0f409e78-a341-4f26-af74-117e4b2775a9 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,56 +1,13 @@
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SubMatchingSelect } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect';
|
||||
import { UnmatchColumnBanner } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumnBanner';
|
||||
import { Column } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Fields } from '@/spreadsheet-import/types';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
AccordionButton as ChakraAccordionButton,
|
||||
} from '@chakra-ui/accordion';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronDown, IconInfoCircle, isDefined } from 'twenty-ui';
|
||||
import { useState } from 'react';
|
||||
import { ExpandableContainer, isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledAccordionButton = styled(ChakraAccordionButton)`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.accent.secondary};
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.accent.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledAccordionContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
`;
|
||||
|
||||
const StyledAccordionLabel = styled.span`
|
||||
color: ${({ theme }) => theme.color.blue};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
align-items: center;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
const StyledIconChevronDown = styled(IconChevronDown)`
|
||||
color: ${({ theme }) => theme.color.blue} !important;
|
||||
`;
|
||||
|
||||
const getAccordionTitle = <T extends string>(
|
||||
const getExpandableContainerTitle = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
column: Column<T>,
|
||||
) => {
|
||||
@ -70,42 +27,51 @@ type UnmatchColumnProps<T extends string> = {
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const UnmatchColumn = <T extends string>({
|
||||
columns,
|
||||
columnIndex,
|
||||
onSubChange,
|
||||
}: UnmatchColumnProps<T>) => {
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const column = columns[columnIndex];
|
||||
const isSelect = 'matchedOptions' in column;
|
||||
|
||||
if (!isSelect) return null;
|
||||
|
||||
return (
|
||||
isSelect && (
|
||||
<StyledAccordionContainer>
|
||||
<Accordion allowMultiple width="100%" height="100%">
|
||||
<AccordionItem border="none" py={1} height="100%">
|
||||
<StyledAccordionButton data-testid="accordion-button">
|
||||
<StyledAccordionLabel>
|
||||
<IconInfoCircle />
|
||||
{getAccordionTitle(fields, column)}
|
||||
</StyledAccordionLabel>
|
||||
<AccordionIcon as={StyledIconChevronDown} />
|
||||
</StyledAccordionButton>
|
||||
<AccordionPanel mt={16} gap={12} display="flex" flexDir="column">
|
||||
{column.matchedOptions.map((option) => (
|
||||
<SubMatchingSelect
|
||||
option={option}
|
||||
column={column}
|
||||
onSubChange={onSubChange}
|
||||
key={option.entry}
|
||||
placeholder="Select an option"
|
||||
/>
|
||||
))}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</StyledAccordionContainer>
|
||||
)
|
||||
<StyledContainer>
|
||||
<UnmatchColumnBanner
|
||||
message={getExpandableContainerTitle(fields, column)}
|
||||
buttonOnClick={() => setIsExpanded(!isExpanded)}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
<ExpandableContainer isExpanded={isExpanded}>
|
||||
<StyledContentWrapper>
|
||||
{column.matchedOptions.map((option) => (
|
||||
<SubMatchingSelect
|
||||
option={option}
|
||||
column={column}
|
||||
onSubChange={onSubChange}
|
||||
key={option.entry}
|
||||
placeholder="Select an option"
|
||||
/>
|
||||
))}
|
||||
</StyledContentWrapper>
|
||||
</ExpandableContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Banner, IconChevronDown, IconInfoCircle } from 'twenty-ui';
|
||||
|
||||
const StyledBanner = styled(Banner)`
|
||||
background: ${({ theme }) => theme.accent.secondary};
|
||||
border-radius: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(2.5)};
|
||||
`;
|
||||
|
||||
const StyledText = styled.div`
|
||||
color: ${({ theme }) => theme.color.blue};
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTransitionedIconChevronDown = styled(IconChevronDown)<{
|
||||
isExpanded: boolean;
|
||||
}>`
|
||||
color: ${({ theme }) => theme.color.blue};
|
||||
transform: ${({ isExpanded }) =>
|
||||
isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'};
|
||||
transition: ${({ theme }) =>
|
||||
`transform ${theme.animation.duration.normal}s ease`};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const UnmatchColumnBanner = ({
|
||||
message,
|
||||
isExpanded,
|
||||
buttonOnClick,
|
||||
}: {
|
||||
message: string;
|
||||
isExpanded: boolean;
|
||||
buttonOnClick?: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledBanner>
|
||||
<IconInfoCircle color={theme.color.blue} size={theme.icon.size.md} />
|
||||
<StyledText>{message}</StyledText>
|
||||
{buttonOnClick && (
|
||||
<StyledTransitionedIconChevronDown
|
||||
isExpanded={isExpanded}
|
||||
onClick={buttonOnClick}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
)}
|
||||
</StyledBanner>
|
||||
);
|
||||
};
|
||||
@ -127,7 +127,6 @@ export default defineConfig(({ command, mode }) => {
|
||||
localsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
path: 'rollup-plugin-node-polyfills/polyfills/path',
|
||||
|
||||
@ -22,12 +22,18 @@ const StyledBanner = styled.div<{ variant?: BannerVariant }>`
|
||||
|
||||
export type BannerVariant = 'danger' | 'default';
|
||||
|
||||
type BannerProps = {
|
||||
variant?: BannerVariant;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const Banner = ({
|
||||
variant = 'default',
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
variant?: BannerVariant;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<StyledBanner variant={variant}>{children}</StyledBanner>
|
||||
}: BannerProps) => (
|
||||
<StyledBanner variant={variant} className={className}>
|
||||
{children}
|
||||
</StyledBanner>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './components';
|
||||
export * from './display';
|
||||
export * from './layout';
|
||||
export * from './testing';
|
||||
export * from './theme';
|
||||
export * from './utilities';
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from '@ui/utilities';
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
const StyledTransitionContainer = styled.div<{
|
||||
isExpanded: boolean;
|
||||
height: number;
|
||||
}>`
|
||||
max-height: ${({ isExpanded, height }) => (isExpanded ? `${height}px` : '0')};
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: max-height
|
||||
${({ theme, isExpanded }) =>
|
||||
`${theme.animation.duration.normal}s ${isExpanded ? 'ease-in' : 'ease-out'}`};
|
||||
`;
|
||||
|
||||
type ExpandableContainerProps = {
|
||||
isExpanded: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ExpandableContainer = ({
|
||||
isExpanded,
|
||||
children,
|
||||
}: ExpandableContainerProps) => {
|
||||
const [contentHeight, setContentHeight] = useState(0);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isDefined(contentRef.current)) {
|
||||
setContentHeight(contentRef.current.scrollHeight);
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
return (
|
||||
<StyledTransitionContainer isExpanded={isExpanded} height={contentHeight}>
|
||||
<div ref={contentRef}>{children}</div>
|
||||
</StyledTransitionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpandableContainer;
|
||||
@ -0,0 +1,81 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { useState } from 'react';
|
||||
import ExpandableContainer from '../ExpandableContainer';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)};
|
||||
background-color: ${({ theme }) => theme.color.blue50};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
cursor: pointer;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.color.blue40};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
height: 200px;
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
|
||||
p {
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
}
|
||||
`;
|
||||
|
||||
const ExpandableContainerWithButton = (args: any) => {
|
||||
const [isExpanded, setIsExpanded] = useState(args.isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledButton onClick={() => setIsExpanded(!isExpanded)}>
|
||||
{isExpanded ? 'Collapse' : 'Expand'}
|
||||
</StyledButton>
|
||||
<ExpandableContainer isExpanded={isExpanded}>
|
||||
<StyledContent>
|
||||
<p>
|
||||
This is some content inside the ExpandableContainer. It will grow
|
||||
and shrink depending on the expand/collapse state.
|
||||
</p>
|
||||
<p>
|
||||
Add more text or even other components here to test how the
|
||||
container handles more content.
|
||||
</p>
|
||||
<p>
|
||||
Feel free to adjust the height and content to see how it affects the
|
||||
expand/collapse behavior.
|
||||
</p>
|
||||
</StyledContent>
|
||||
</ExpandableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ExpandableContainer> = {
|
||||
title: 'UI/Layout/ExpandableContainer',
|
||||
component: ExpandableContainerWithButton,
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
isExpanded: {
|
||||
control: 'boolean',
|
||||
description: 'Controls whether the container is expanded or collapsed',
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ExpandableContainerWithButton>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isExpanded: false,
|
||||
},
|
||||
};
|
||||
1
packages/twenty-ui/src/layout/index.ts
Normal file
1
packages/twenty-ui/src/layout/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './expandableContainer/components/ExpandableContainer';
|
||||
Reference in New Issue
Block a user