Update bar chart design (#15372)

- Implement new dynamic color palettes
- Create custom bar component with rounded edges



https://github.com/user-attachments/assets/8246f16d-0239-4807-bb4a-9647367575f2
This commit is contained in:
Raphaël Bosi
2025-10-27 16:46:06 +00:00
committed by GitHub
parent e975fbe217
commit 01d8b99c38
29 changed files with 593 additions and 333 deletions
@@ -117,7 +117,7 @@ export const ChartSettingItem = ({
id={item.id}
dropdownId={item.id}
dropdownComponents={
<DropdownContent>
<DropdownContent widthInPixels={item.dropdownWidth}>
{item.DropdownContent && <item.DropdownContent />}
</DropdownContent>
}
@@ -0,0 +1,76 @@
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
import { generateGroupColor } from '@/page-layout/widgets/graph/utils/generateGroupColor';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ColorSample } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type ThemeColor } from 'twenty-ui/theme';
type ChartColorGradientOptionProps = {
colorOption: {
id: string;
name: string;
colorName: ThemeColor | 'auto';
};
selectedItemId: string | null;
currentColor: string | null | undefined;
onSelectColor: (colorName: ThemeColor | 'auto') => void;
};
const StyledColorSamplesContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(0.5)};
`;
const COLOR_GROUP_COUNT = 5;
export const ChartColorGradientOption = ({
colorOption,
selectedItemId,
currentColor,
onSelectColor,
}: ChartColorGradientOptionProps) => {
const colorName = colorOption.colorName as ThemeColor;
const theme = useTheme();
const colorRegistry = createGraphColorRegistry(theme);
const colorSamples = (
<StyledColorSamplesContainer>
{Array.from({ length: COLOR_GROUP_COUNT }).map((_, index) => {
const colorScheme = colorRegistry[colorName];
const reversedIndex = COLOR_GROUP_COUNT - 1 - index;
const groupColor = generateGroupColor({
colorScheme,
groupIndex: reversedIndex,
totalGroups: COLOR_GROUP_COUNT,
});
return <ColorSample key={index} color={groupColor} />;
})}
</StyledColorSamplesContainer>
);
return (
<SelectableListItem
key={colorOption.id}
itemId={colorOption.id}
onEnter={() => {
onSelectColor(colorOption.colorName);
}}
>
<MenuItemSelect
text={colorOption.name}
selected={false}
focused={
selectedItemId === colorOption.id || currentColor === colorOption.id
}
contextualText={colorSamples}
contextualTextPosition="right"
onClick={() => {
onSelectColor(colorOption.colorName);
}}
/>
</SelectableListItem>
);
};
@@ -0,0 +1,61 @@
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import styled from '@emotion/styled';
import { ColorSample } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { MAIN_COLORS, type ThemeColor } from 'twenty-ui/theme';
type ChartColorPaletteOptionProps = {
selectedItemId: string | null;
currentColor: string | null | undefined;
onSelectColor: (colorName: ThemeColor | 'auto') => void;
};
const StyledColorSamplesContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(0.5)};
`;
export const ChartColorPaletteOption = ({
selectedItemId,
currentColor,
onSelectColor,
}: ChartColorPaletteOptionProps) => {
const paletteColors: Array<keyof typeof MAIN_COLORS> = [
'purple',
'pink',
'red',
'orange',
'yellow',
];
const colorSamples = (
<StyledColorSamplesContainer>
{paletteColors.map((paletteColorName) => {
const baseColor = MAIN_COLORS[paletteColorName];
return <ColorSample key={paletteColorName} color={baseColor} />;
})}
</StyledColorSamplesContainer>
);
return (
<SelectableListItem
key={'auto'}
itemId={'auto'}
onEnter={() => {
onSelectColor('auto');
}}
>
<MenuItemSelect
text={'Palette'}
selected={false}
focused={selectedItemId === 'auto' || currentColor === 'auto'}
contextualText={colorSamples}
contextualTextPosition="right"
onClick={() => {
onSelectColor('auto');
}}
/>
</SelectableListItem>
);
};
@@ -1,24 +1,30 @@
import { ChartColorGradientOption } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartColorGradientOption';
import { ChartColorPaletteOption } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartColorPaletteOption';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { ColorSample } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { MAIN_COLOR_NAMES, type ThemeColor } from 'twenty-ui/theme';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type ColorOption = {
id: string;
name: string;
colorName: ThemeColor | 'auto';
};
export const ChartColorSelectionDropdownContent = () => {
const [searchQuery, setSearchQuery] = useState('');
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -54,11 +60,18 @@ export const ChartColorSelectionDropdownContent = () => {
const currentColor = configuration.color;
const colorOptions = MAIN_COLOR_NAMES.map((colorName) => ({
id: colorName,
name: capitalize(colorName),
colorName: colorName,
}));
const colorOptions: ColorOption[] = [
{
id: 'auto',
name: 'Palette',
colorName: 'auto',
},
...MAIN_COLOR_NAMES.map((colorName) => ({
id: colorName,
name: capitalize(colorName),
colorName: colorName,
})),
];
const filteredColorOptions = filterBySearchQuery({
items: colorOptions,
@@ -66,7 +79,7 @@ export const ChartColorSelectionDropdownContent = () => {
getSearchableValues: (item) => [item.name],
});
const handleSelectColor = (colorName: ThemeColor) => {
const handleSelectColor = (colorName: ThemeColor | 'auto') => {
updateCurrentWidgetConfig({
configToUpdate: {
color: colorName,
@@ -75,6 +88,10 @@ export const ChartColorSelectionDropdownContent = () => {
closeDropdown();
};
const regularColorOptions = filteredColorOptions.filter(
(option) => option.colorName !== 'auto',
);
return (
<>
<DropdownMenuSearchInput
@@ -84,6 +101,7 @@ export const ChartColorSelectionDropdownContent = () => {
onChange={(event) => setSearchQuery(event.target.value)}
value={searchQuery}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
@@ -92,29 +110,37 @@ export const ChartColorSelectionDropdownContent = () => {
(colorOption) => colorOption.id,
)}
>
{filteredColorOptions.map((colorOption) => (
<SelectableListItem
key={colorOption.id}
itemId={colorOption.id}
onEnter={() => {
handleSelectColor(colorOption.colorName);
}}
>
<MenuItemSelect
text={colorOption.name}
selected={currentColor === colorOption.id}
focused={selectedItemId === colorOption.id}
LeftIcon={() => (
<ColorSample colorName={colorOption.colorName} />
)}
onClick={() => {
handleSelectColor(colorOption.colorName);
}}
/>
</SelectableListItem>
))}
<ChartColorPaletteOption
selectedItemId={selectedItemId}
currentColor={currentColor}
onSelectColor={handleSelectColor}
/>
</SelectableList>
</DropdownMenuItemsContainer>
{regularColorOptions.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={filteredColorOptions.map(
(colorOption) => colorOption.id,
)}
>
{regularColorOptions.map((colorOption) => (
<ChartColorGradientOption
key={colorOption.id}
colorOption={colorOption}
selectedItemId={selectedItemId}
currentColor={currentColor}
onSelectColor={handleSelectColor}
/>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</>
)}
</>
);
};
@@ -2,6 +2,7 @@ import { ChartColorSelectionDropdownContent } from '@/command-menu/pages/page-la
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { IconColorSwatch } from 'twenty-ui/display';
export const COLORS_SETTING: ChartSettingsItem = {
@@ -10,4 +11,5 @@ export const COLORS_SETTING: ChartSettingsItem = {
label: CHART_CONFIGURATION_SETTING_LABELS.COLORS,
id: CHART_CONFIGURATION_SETTING_IDS.COLORS,
DropdownContent: ChartColorSelectionDropdownContent,
dropdownWidth: GenericDropdownContentWidth.ExtraLarge,
};
@@ -16,6 +16,7 @@ export type ChartSettingsItem = {
isBoolean: boolean;
dependsOn?: CHART_CONFIGURATION_SETTING_IDS[];
DropdownContent?: ComponentType;
dropdownWidth?: number;
isInput?: boolean;
inputPlaceholder?: MessageDescriptor;
};
@@ -0,0 +1 @@
export const GRAPH_GROUP_COLOR_MINIMUM_ALPHA = 0.2;
@@ -0,0 +1 @@
export const GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS = 10;
@@ -1,45 +0,0 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey';
import { calculateBarChartEndLineCoordinates } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateBarChartEndLineCoordinates';
import { type ComputedBarDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
type BarChartEndLinesProps = {
bars: readonly ComputedBarDatum<BarChartDataItem>[];
enrichedKeysMap: Map<string, BarChartEnrichedKey>;
layout: 'vertical' | 'horizontal';
};
export const BarChartEndLines = ({
bars,
enrichedKeysMap,
layout,
}: BarChartEndLinesProps) => {
return (
<g>
{bars.map((bar: ComputedBarDatum<BarChartDataItem>, index: number) => {
const enrichedKey = enrichedKeysMap.get(String(bar.data.id));
if (!isDefined(enrichedKey)) {
return null;
}
const lineColor = enrichedKey.colorScheme.solid;
const { x1, y1, x2, y2 } = calculateBarChartEndLineCoordinates(
bar,
layout,
);
return (
<line
key={`${bar.data.id}-${bar.data.indexValue}-endline-${index}`}
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={lineColor}
strokeWidth={1}
/>
);
})}
</g>
);
};
@@ -0,0 +1,211 @@
import { BAR_CHART_HOVER_BRIGHTNESS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartHoverBrightness';
import { type BarDatum, type BarItemProps } from '@nivo/bar';
import { Text } from '@nivo/text';
import { useTheme } from '@nivo/theming';
import { useTooltip } from '@nivo/tooltip';
import { animated, to } from '@react-spring/web';
import { isNumber } from '@sniptt/guards';
import { createElement, useCallback, useMemo, type MouseEvent } from 'react';
import styled from 'styled-components';
import { isDefined } from 'twenty-shared/utils';
type CustomBarItemProps<D extends BarDatum> = BarItemProps<D> & {
keys?: string[];
groupMode?: 'grouped' | 'stacked';
data?: readonly D[];
indexBy?: string;
layout?: 'vertical' | 'horizontal';
};
const StyledBarRect = styled(animated.rect)<{ $isInteractive?: boolean }>`
cursor: ${({ $isInteractive }) => ($isInteractive ? 'pointer' : 'default')};
transition: filter 0.15s ease-in-out;
&:hover {
filter: ${({ $isInteractive }) =>
$isInteractive ? `brightness(${BAR_CHART_HOVER_BRIGHTNESS})` : 'none'};
}
`;
// This is a copy of the BarItem component from @nivo/bar with some design modifications
export const CustomBarItem = <D extends BarDatum>({
bar: { data: barData, ...bar },
style: {
borderColor,
color,
height,
labelColor,
labelOpacity,
labelX,
labelY,
transform,
width,
textAnchor,
},
borderRadius,
borderWidth,
label,
shouldRenderLabel,
isInteractive,
onClick,
onMouseEnter,
onMouseLeave,
tooltip,
isFocusable,
ariaLabel,
ariaLabelledBy,
ariaDescribedBy,
ariaDisabled,
ariaHidden,
keys,
groupMode = 'grouped',
data: chartData,
indexBy,
layout = 'vertical',
}: CustomBarItemProps<D>) => {
const theme = useTheme();
const { showTooltipFromEvent, showTooltipAt, hideTooltip } = useTooltip();
const renderTooltip = useMemo(
() => () => createElement(tooltip, { ...bar, ...barData }),
[tooltip, bar, barData],
);
const handleClick = useCallback(
(event: MouseEvent<SVGRectElement>) => {
onClick?.({ color: bar.color, ...barData }, event);
},
[bar, barData, onClick],
);
const handleTooltip = useCallback(
(event: MouseEvent<SVGRectElement>) =>
showTooltipFromEvent(renderTooltip(), event),
[showTooltipFromEvent, renderTooltip],
);
const handleMouseEnter = useCallback(
(event: MouseEvent<SVGRectElement>) => {
onMouseEnter?.(barData, event);
showTooltipFromEvent(renderTooltip(), event);
},
[barData, onMouseEnter, showTooltipFromEvent, renderTooltip],
);
const handleMouseLeave = useCallback(
(event: MouseEvent<SVGRectElement>) => {
onMouseLeave?.(barData, event);
hideTooltip();
},
[barData, hideTooltip, onMouseLeave],
);
const handleFocus = useCallback(() => {
showTooltipAt(renderTooltip(), [bar.absX + bar.width / 2, bar.absY]);
}, [showTooltipAt, renderTooltip, bar]);
const handleBlur = useCallback(() => {
hideTooltip();
}, [hideTooltip]);
const isTopBar = useMemo(() => {
const isStackedAndValid =
groupMode === 'stacked' &&
isDefined(keys) &&
keys.length > 0 &&
isDefined(chartData) &&
isDefined(indexBy);
if (!isStackedAndValid) {
return true;
}
const dataPoint = chartData.find(
(data) => data[indexBy] === barData.indexValue,
);
if (!isDefined(dataPoint)) {
return true;
}
const currentKeyIndex = keys.findIndex((key) => key === barData.id);
if (currentKeyIndex === -1) {
return true;
}
const keysAboveCurrentKey = keys.slice(currentKeyIndex + 1);
const hasBarAbove = keysAboveCurrentKey.some((key) => {
const value = dataPoint[key];
return isNumber(value) && value > 0;
});
return !hasBarAbove;
}, [groupMode, keys, barData, chartData, indexBy]);
const isHorizontal = layout === 'horizontal';
return (
<animated.g transform={transform}>
{isTopBar && (
<defs>
<clipPath id={`round-corner-${barData.index}`}>
<animated.rect
x={isHorizontal ? -borderRadius : 0}
y={0}
rx={borderRadius}
ry={borderRadius}
width={to(width, (value) =>
Math.max(value + (isHorizontal ? borderRadius : 0), 0),
)}
height={to(height, (value) =>
Math.max(value + (isHorizontal ? 0 : borderRadius), 0),
)}
/>
</clipPath>
</defs>
)}
<StyledBarRect
$isInteractive={isInteractive}
clipPath={isTopBar ? `url(#round-corner-${barData.index})` : undefined}
width={to(width, (value) => Math.max(value, 0))}
height={to(height, (value) => Math.max(value, 0))}
fill={color}
strokeWidth={borderWidth}
stroke={borderColor}
focusable={isFocusable}
tabIndex={isFocusable ? 0 : undefined}
aria-label={ariaLabel ? ariaLabel(barData) : undefined}
aria-labelledby={ariaLabelledBy ? ariaLabelledBy(barData) : undefined}
aria-describedby={
ariaDescribedBy ? ariaDescribedBy(barData) : undefined
}
aria-disabled={ariaDisabled ? ariaDisabled(barData) : undefined}
aria-hidden={ariaHidden ? ariaHidden(barData) : undefined}
onMouseEnter={isInteractive ? handleMouseEnter : undefined}
onMouseMove={isInteractive ? handleTooltip : undefined}
onMouseLeave={isInteractive ? handleMouseLeave : undefined}
onClick={isInteractive ? handleClick : undefined}
onFocus={isInteractive && isFocusable ? handleFocus : undefined}
onBlur={isInteractive && isFocusable ? handleBlur : undefined}
data-testid={`bar.item.${barData.id}.${barData.index}`}
/>
{shouldRenderLabel && (
<Text
x={labelX}
y={labelY}
textAnchor={textAnchor}
dominantBaseline="central"
fillOpacity={labelOpacity}
style={{
...theme.labels.text,
// We don't want the label to intercept mouse events
pointerEvents: 'none',
fill: labelColor,
}}
>
{label}
</Text>
)}
</animated.g>
);
};
@@ -1,7 +1,7 @@
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend';
import { GraphWidgetTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetTooltip';
import { BarChartEndLines } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines';
import { CustomBarItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem';
import { BAR_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins';
import { useBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData';
import { useBarChartHandlers } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartHandlers';
@@ -19,8 +19,8 @@ import {
import { NodeDimensionEffect } from '@/ui/utilities/dimensions/components/NodeDimensionEffect';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ResponsiveBar, type BarCustomLayerProps } from '@nivo/bar';
import { useId, useRef, useState } from 'react';
import { ResponsiveBar } from '@nivo/bar';
import { useMemo, useRef, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
const LEGEND_THRESHOLD = 10;
@@ -76,7 +76,6 @@ export const GraphWidgetBarChart = ({
customFormatter,
}: GraphWidgetBarChartProps) => {
const theme = useTheme();
const instanceId = useId();
const colorRegistry = createGraphColorRegistry(theme);
const [chartWidth, setChartWidth] = useState<number>(0);
const [chartHeight, setChartHeight] = useState<number>(0);
@@ -98,17 +97,13 @@ export const GraphWidgetBarChart = ({
const chartTheme = useBarChartTheme();
const { barConfigs, enrichedKeys, enrichedKeysMap, defs } = useBarChartData({
const { barConfigs, enrichedKeys } = useBarChartData({
data,
indexBy,
keys,
series,
colorRegistry,
id,
instanceId,
seriesLabels,
hoveredBar,
layout,
});
const { renderTooltip: getTooltipData } = useBarChartTooltip({
@@ -152,22 +147,27 @@ export const GraphWidgetBarChart = ({
);
};
const barEndLinesLayer = (props: BarCustomLayerProps<BarChartDataItem>) => {
return (
<BarChartEndLines
bars={props.bars}
enrichedKeysMap={enrichedKeysMap}
const BarItemWithContext = useMemo(
() => (props: any) => (
<CustomBarItem
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
keys={keys}
groupMode={groupMode}
data={data}
indexBy={indexBy}
layout={layout}
/>
);
};
),
[keys, groupMode, data, indexBy, layout],
);
return (
<StyledContainer id={id}>
<GraphWidgetChartContainer
ref={containerRef}
$isClickable={hasClickableItems}
$cursorSelector='svg g[transform] rect[fill^="url(#gradient-"]'
$cursorSelector="svg g[transform] rect[fill]"
>
<NodeDimensionEffect
elementRef={containerRef}
@@ -177,6 +177,7 @@ export const GraphWidgetBarChart = ({
}}
/>
<ResponsiveBar
barComponent={BarItemWithContext}
data={data}
keys={keys}
indexBy={indexBy}
@@ -192,15 +193,7 @@ export const GraphWidgetBarChart = ({
}}
indexScale={{ type: 'band', round: true }}
colors={(datum) => getBarChartColor(datum, barConfigs, theme)}
defs={defs}
layers={[
'grid',
'axes',
'bars',
barEndLinesLayer,
'markers',
'legends',
]}
layers={['grid', 'axes', 'bars', 'markers', 'legends']}
axisTop={null}
axisRight={null}
axisBottom={axisBottomConfig}
@@ -226,6 +219,7 @@ export const GraphWidgetBarChart = ({
}}
onMouseLeave={() => setHoveredBar(null)}
theme={chartTheme}
borderRadius={parseInt(theme.border.radius.sm)}
/>
</GraphWidgetChartContainer>
<GraphWidgetLegend
@@ -0,0 +1 @@
export const BAR_CHART_HOVER_BRIGHTNESS = 0.85;
@@ -47,10 +47,6 @@ describe('useBarChartData', () => {
keys: ['sales', 'costs'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: null,
layout: 'vertical',
}),
);
@@ -66,10 +62,6 @@ describe('useBarChartData', () => {
keys: ['sales', 'costs'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: null,
layout: 'vertical',
}),
);
@@ -77,14 +69,18 @@ describe('useBarChartData', () => {
expect(result.current.barConfigs[0]).toMatchObject({
key: 'sales',
indexValue: 'Jan',
gradientId: 'gradient-test-chart-instance-1-sales-0-0',
colorScheme: mockColorRegistry.green,
colorScheme: {
name: 'green',
gradient: mockColorRegistry.green.gradient,
},
});
expect(result.current.barConfigs[1]).toMatchObject({
key: 'costs',
indexValue: 'Jan',
gradientId: 'gradient-test-chart-instance-1-costs-0-1',
colorScheme: mockColorRegistry.purple,
colorScheme: {
name: 'purple',
gradient: mockColorRegistry.purple.gradient,
},
});
});
@@ -96,25 +92,28 @@ describe('useBarChartData', () => {
keys: ['sales', 'costs'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: null,
layout: 'vertical',
}),
);
expect(result.current.enrichedKeys).toEqual([
{
key: 'sales',
colorScheme: mockColorRegistry.green,
label: 'Sales',
expect(result.current.enrichedKeys).toHaveLength(2);
expect(result.current.enrichedKeys[0]).toMatchObject({
key: 'sales',
label: 'Sales',
colorScheme: {
name: 'green',
gradient: mockColorRegistry.green.gradient,
},
{
key: 'costs',
colorScheme: mockColorRegistry.purple,
label: 'Costs',
});
expect(result.current.enrichedKeys[0].colorScheme.solid).toBeDefined();
expect(result.current.enrichedKeys[1]).toMatchObject({
key: 'costs',
label: 'Costs',
colorScheme: {
name: 'purple',
gradient: mockColorRegistry.purple.gradient,
},
]);
});
expect(result.current.enrichedKeys[1].colorScheme.solid).toBeDefined();
});
it('should use series labels when series config is not provided', () => {
@@ -125,11 +124,7 @@ describe('useBarChartData', () => {
keys: ['sales', 'costs'],
series: undefined,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
seriesLabels: { sales: 'Revenue', costs: 'Expenses' },
hoveredBar: null,
layout: 'vertical',
}),
);
@@ -137,70 +132,6 @@ describe('useBarChartData', () => {
expect(result.current.enrichedKeys[1].label).toBe('Expenses');
});
it('should handle hover state for bars', () => {
const { result } = renderHook(() =>
useBarChartData({
data: mockData,
indexBy: 'month',
keys: ['sales', 'costs'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: { key: 'sales', indexValue: 'Feb' },
layout: 'vertical',
}),
);
const hoveredDef = result.current.defs.find(
(def) => def.id === 'gradient-test-chart-instance-1-sales-1-0',
);
expect(hoveredDef?.colors).toEqual([
{ offset: 0, color: 'green4' },
{ offset: 100, color: 'green3' },
]);
});
it('should generate vertical gradients for vertical layout', () => {
const { result } = renderHook(() =>
useBarChartData({
data: mockData,
indexBy: 'month',
keys: ['sales'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: null,
layout: 'vertical',
}),
);
const def = result.current.defs[0];
expect(def.y1).toBe('0%');
expect(def.y2).toBe('100%');
});
it('should generate horizontal gradients for horizontal layout', () => {
const { result } = renderHook(() =>
useBarChartData({
data: mockData,
indexBy: 'month',
keys: ['sales'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: null,
layout: 'horizontal',
}),
);
const def = result.current.defs[0];
expect(def.x1).toBe('0%');
expect(def.x2).toBe('100%');
});
it('should handle empty data', () => {
const { result } = renderHook(() =>
useBarChartData({
@@ -209,15 +140,10 @@ describe('useBarChartData', () => {
keys: ['sales', 'costs'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: null,
layout: 'vertical',
}),
);
expect(result.current.barConfigs).toEqual([]);
expect(result.current.defs).toEqual([]);
});
it('should handle empty keys', () => {
@@ -228,16 +154,11 @@ describe('useBarChartData', () => {
keys: [],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
hoveredBar: null,
layout: 'vertical',
}),
);
expect(result.current.barConfigs).toEqual([]);
expect(result.current.enrichedKeys).toEqual([]);
expect(result.current.defs).toEqual([]);
});
it('should fall back to key name when no label is provided', () => {
@@ -248,40 +169,11 @@ describe('useBarChartData', () => {
keys: ['sales', 'costs'],
series: undefined,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
seriesLabels: undefined,
hoveredBar: null,
layout: 'vertical',
}),
);
expect(result.current.enrichedKeys[0].label).toBe('sales');
expect(result.current.enrichedKeys[1].label).toBe('costs');
});
it('should recalculate when instanceId changes', () => {
const { result, rerender } = renderHook(
({ instanceId }) =>
useBarChartData({
data: mockData,
indexBy: 'month',
keys: ['sales', 'costs'],
series: mockSeries,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId,
hoveredBar: null,
layout: 'vertical',
}),
{ initialProps: { instanceId: 'instance-1' } },
);
const firstBarConfigs = result.current.barConfigs;
rerender({ instanceId: 'instance-2' });
expect(result.current.barConfigs).not.toBe(firstBarConfigs);
expect(result.current.barConfigs[0].gradientId).toContain('instance-2');
});
});
@@ -3,7 +3,6 @@ import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBa
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry';
import { createGradientDef } from '@/page-layout/widgets/graph/utils/createGradientDef';
import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme';
import { useMemo } from 'react';
@@ -13,11 +12,8 @@ type UseBarChartDataProps = {
keys: string[];
series?: BarChartSeries[];
colorRegistry: GraphColorRegistry;
id: string;
instanceId: string;
seriesLabels?: Record<string, string>;
hoveredBar: { key: string; indexValue: string | number } | null;
layout: 'vertical' | 'horizontal';
groupMode?: 'grouped' | 'stacked';
};
export const useBarChartData = ({
@@ -26,11 +22,7 @@ export const useBarChartData = ({
keys,
series,
colorRegistry,
id,
instanceId,
seriesLabels,
hoveredBar,
layout,
}: UseBarChartDataProps) => {
const seriesConfigMap = useMemo(
() => new Map<string, BarChartSeries>(series?.map((s) => [s.key, s]) || []),
@@ -38,35 +30,35 @@ export const useBarChartData = ({
);
const barConfigs = useMemo((): BarChartConfig[] => {
return data.flatMap((dataPoint, dataIndex) => {
return data.flatMap((dataPoint) => {
const indexValue = dataPoint[indexBy];
return keys.map((key, keyIndex): BarChartConfig => {
const seriesConfig = seriesConfigMap.get(key);
const colorScheme = getColorScheme(
colorRegistry,
seriesConfig?.color,
keyIndex,
);
const sanitizedKey = key.replace(/\s+/g, '-');
const gradientId = `gradient-${id}-${instanceId}-${sanitizedKey}-${dataIndex}-${keyIndex}`;
const colorScheme = getColorScheme({
registry: colorRegistry,
colorName: seriesConfig?.color,
fallbackIndex: keyIndex,
totalGroups: keys.length,
});
return {
key,
indexValue,
gradientId,
colorScheme,
};
});
});
}, [data, indexBy, keys, colorRegistry, id, instanceId, seriesConfigMap]);
}, [data, indexBy, keys, colorRegistry, seriesConfigMap]);
const enrichedKeys: BarChartEnrichedKey[] = keys.map((key, index) => {
const seriesConfig = seriesConfigMap.get(key);
const colorScheme = getColorScheme(
colorRegistry,
seriesConfig?.color,
index,
);
const colorScheme = getColorScheme({
registry: colorRegistry,
colorName: seriesConfig?.color,
fallbackIndex: index,
totalGroups: keys.length,
});
return {
key,
colorScheme,
@@ -74,28 +66,9 @@ export const useBarChartData = ({
};
});
const enrichedKeysMap = useMemo(
() => new Map(enrichedKeys.map((item) => [item.key, item])),
[enrichedKeys],
);
const defs = barConfigs.map((bar) => {
const isHovered =
hoveredBar?.key === bar.key && hoveredBar?.indexValue === bar.indexValue;
return createGradientDef(
bar.colorScheme,
bar.gradientId,
isHovered,
layout === 'horizontal' ? 0 : 90,
true,
);
});
return {
seriesConfigMap,
barConfigs,
enrichedKeys,
enrichedKeysMap,
defs,
};
};
@@ -3,6 +3,5 @@ import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphCo
export type BarChartConfig = {
key: string;
indexValue: string | number;
gradientId: string;
colorScheme: GraphColorScheme;
};
@@ -14,5 +14,5 @@ export const getBarChartColor = (
if (!isDefined(bar)) {
return theme.border.color.light;
}
return `url(#${bar.gradientId})`;
return bar.colorScheme.solid;
};
@@ -22,7 +22,11 @@ export const useGaugeChartData = ({
const { value, min, max, color = 'blue' } = data;
const colorScheme = useMemo(
() => getColorScheme(colorRegistry, color),
() =>
getColorScheme({
registry: colorRegistry,
colorName: color,
}),
[colorRegistry, color],
);
@@ -80,17 +80,11 @@ describe('useLineChartData', () => {
}),
);
expect(result.current.enrichedSeries[0].colorScheme).toBe(
mockColorRegistry.red,
);
expect(result.current.enrichedSeries[0].gradientId).toBe(
'lineGradient-test-chart-instance-1-series1-0',
);
expect(result.current.enrichedSeries[0].label).toBe('Sales');
expect(result.current.enrichedSeries[1].colorScheme).toBe(
mockColorRegistry.blue,
);
expect(result.current.enrichedSeries[1].label).toBe('Costs');
});
@@ -268,22 +262,6 @@ describe('useLineChartData', () => {
]);
});
it('should extract colors for series', () => {
const { result } = renderHook(() =>
useLineChartData({
data: mockData,
colorRegistry: mockColorRegistry,
id: 'test-chart',
instanceId: 'instance-1',
enableArea: false,
theme: mockTheme,
formatOptions: mockFormatOptions,
}),
);
expect(result.current.colors).toEqual(['redSolid', 'blueSolid']);
});
it('should calculate legend items with totals', () => {
const { result } = renderHook(() =>
useLineChartData({
@@ -302,13 +280,13 @@ describe('useLineChartData', () => {
id: 'series1',
label: 'Sales',
formattedValue: '370',
color: 'redSolid',
color: expect.any(String),
},
{
id: 'series2',
label: 'Costs',
formattedValue: '270',
color: 'blueSolid',
color: expect.any(String),
},
]);
});
@@ -34,7 +34,12 @@ export const useLineChartData = ({
const dataMap = Object.fromEntries(data.map((series) => [series.id, series]));
const enrichedSeries = useMemo((): LineChartEnrichedSeries[] => {
return data.map((series, index) => {
const colorScheme = getColorScheme(colorRegistry, series.color, index);
const colorScheme = getColorScheme({
registry: colorRegistry,
colorName: series.color,
fallbackIndex: index,
totalGroups: data.length,
});
const shouldEnableArea = series.enableArea ?? enableArea;
const gradientId = `lineGradient-${id}-${instanceId}-${series.id}-${index}`;
@@ -1,12 +1,12 @@
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData';
import { useMemo } from 'react';
import { calculatePieChartAngles } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartAngles';
import { calculatePieChartPercentage } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartPercentage';
import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry';
import { createGradientDef } from '@/page-layout/widgets/graph/utils/createGradientDef';
import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme';
import { type DatumId } from '@nivo/pie';
import { useMemo } from 'react';
type UsePieChartDataProps = {
data: PieChartDataItem[];
@@ -26,7 +26,13 @@ export const usePieChartData = ({
let cumulativeAngle = 0;
return data.map((item, index) => {
const colorScheme = getColorScheme(colorRegistry, item.color, index);
const colorScheme = getColorScheme({
registry: colorRegistry,
colorName: item.color,
fallbackIndex: index,
totalGroups: data.length,
});
const isHovered = hoveredSliceId === item.id;
const gradientId = `${colorScheme.name}Gradient-${id}-${index}`;
const percentage = calculatePieChartPercentage(item.value, totalValue);
@@ -1,4 +1,5 @@
export type GraphColor =
| 'auto'
| 'blue'
| 'purple'
| 'turquoise'
@@ -11,7 +11,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.blue1, theme.adaptiveColors.blue2],
hover: [theme.adaptiveColors.blue3, theme.adaptiveColors.blue4],
},
solid: theme.color.blue,
solid: theme.adaptiveColors.blue4,
},
purple: {
name: 'purple',
@@ -19,7 +19,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.purple1, theme.adaptiveColors.purple2],
hover: [theme.adaptiveColors.purple3, theme.adaptiveColors.purple4],
},
solid: theme.color.purple,
solid: theme.adaptiveColors.purple4,
},
turquoise: {
name: 'turquoise',
@@ -30,7 +30,7 @@ export const createGraphColorRegistry = (
],
hover: [theme.adaptiveColors.turquoise3, theme.adaptiveColors.turquoise4],
},
solid: theme.color.turquoise,
solid: theme.adaptiveColors.turquoise4,
},
orange: {
name: 'orange',
@@ -38,7 +38,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.orange1, theme.adaptiveColors.orange2],
hover: [theme.adaptiveColors.orange3, theme.adaptiveColors.orange4],
},
solid: theme.color.orange,
solid: theme.adaptiveColors.orange4,
},
pink: {
name: 'pink',
@@ -46,7 +46,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.pink1, theme.adaptiveColors.pink2],
hover: [theme.adaptiveColors.pink3, theme.adaptiveColors.pink4],
},
solid: theme.color.pink,
solid: theme.adaptiveColors.pink4,
},
yellow: {
name: 'yellow',
@@ -54,7 +54,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.yellow1, theme.adaptiveColors.yellow2],
hover: [theme.adaptiveColors.yellow3, theme.adaptiveColors.yellow4],
},
solid: theme.color.yellow,
solid: theme.adaptiveColors.yellow4,
},
red: {
name: 'red',
@@ -62,7 +62,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.red1, theme.adaptiveColors.red2],
hover: [theme.adaptiveColors.red3, theme.adaptiveColors.red4],
},
solid: theme.color.red,
solid: theme.adaptiveColors.red4,
},
green: {
name: 'green',
@@ -70,7 +70,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.green1, theme.adaptiveColors.green2],
hover: [theme.adaptiveColors.green3, theme.adaptiveColors.green4],
},
solid: theme.color.green,
solid: theme.adaptiveColors.green4,
},
sky: {
name: 'sky',
@@ -78,7 +78,7 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.sky1, theme.adaptiveColors.sky2],
hover: [theme.adaptiveColors.sky3, theme.adaptiveColors.sky4],
},
solid: theme.color.sky,
solid: theme.adaptiveColors.sky4,
},
gray: {
name: 'gray',
@@ -86,6 +86,6 @@ export const createGraphColorRegistry = (
normal: [theme.adaptiveColors.gray1, theme.adaptiveColors.gray2],
hover: [theme.adaptiveColors.gray3, theme.adaptiveColors.gray4],
},
solid: theme.color.gray,
solid: theme.adaptiveColors.gray4,
},
});
@@ -0,0 +1,32 @@
import { RGBA } from 'twenty-ui/theme';
import { GRAPH_GROUP_COLOR_MINIMUM_ALPHA } from '@/page-layout/widgets/graph/constants/GraphGroupColorMinimumAlpha.constant';
import { GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroupColors';
import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphColorScheme';
export const generateGroupColor = ({
colorScheme,
groupIndex,
totalGroups,
}: {
colorScheme: GraphColorScheme;
groupIndex: number;
totalGroups: number;
}): string => {
if (totalGroups <= 1) {
return colorScheme.solid;
}
const effectiveGroupIndex = groupIndex % GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS;
const effectiveTotalGroups = Math.min(
totalGroups,
GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS,
);
const ratio = (effectiveGroupIndex + 1) / effectiveTotalGroups;
const alpha =
GRAPH_GROUP_COLOR_MINIMUM_ALPHA +
(1 - GRAPH_GROUP_COLOR_MINIMUM_ALPHA) * ratio;
return RGBA(colorScheme.solid, alpha);
};
@@ -1,16 +1,35 @@
import { generateGroupColor } from '@/page-layout/widgets/graph/utils/generateGroupColor';
import { getColorSchemeByIndex } from '@/page-layout/widgets/graph/utils/getColorSchemeByIndex';
import { isDefined } from 'twenty-shared/utils';
import { type GraphColor } from '../types/GraphColor';
import { type GraphColorRegistry } from '../types/GraphColorRegistry';
import { type GraphColorScheme } from '../types/GraphColorScheme';
import { getColorSchemeByIndex } from './getColorSchemeByIndex';
export const getColorScheme = (
registry: GraphColorRegistry,
colorName?: GraphColor,
fallbackIndex?: number,
): GraphColorScheme => {
if (isDefined(colorName) && isDefined(registry[colorName])) {
export const getColorScheme = ({
registry,
colorName,
fallbackIndex,
totalGroups,
}: {
registry: GraphColorRegistry;
colorName?: GraphColor;
fallbackIndex?: number;
totalGroups?: number;
}): GraphColorScheme => {
if (!isDefined(colorName) || !isDefined(registry[colorName])) {
return getColorSchemeByIndex(registry, fallbackIndex ?? 0);
}
if (!isDefined(totalGroups)) {
return registry[colorName];
}
return getColorSchemeByIndex(registry, fallbackIndex || 0);
return {
...registry[colorName],
solid: generateGroupColor({
colorScheme: registry[colorName],
groupIndex: fallbackIndex ?? 0,
totalGroups,
}),
};
};
@@ -4,6 +4,7 @@ import { type ExtendedAggregateOperations } from '@/object-record/record-table/t
import { GRAPH_MAXIMUM_NUMBER_OF_GROUPS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroups.constant';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
@@ -115,6 +116,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
const series: BarChartSeries[] = keys.map((key) => ({
key,
label: key,
color: configuration.color as GraphColor,
}));
return {
@@ -176,6 +176,7 @@ export const getPageLayoutWidgetDataSeeds = (
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
color: 'auto',
}
: null,
objectMetadataId: opportunityObject?.id ?? null,
@@ -204,6 +205,7 @@ export const getPageLayoutWidgetDataSeeds = (
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
color: 'auto',
}
: null,
objectMetadataId: rocketObject?.id ?? null,
@@ -306,6 +308,7 @@ export const getPageLayoutWidgetDataSeeds = (
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
color: 'auto',
}
: null,
objectMetadataId: companyObject?.id ?? null,
@@ -471,6 +474,7 @@ export const getPageLayoutWidgetDataSeeds = (
primaryAxisOrderBy: 'VALUE_DESC',
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
color: 'auto',
}
: null,
objectMetadataId: personObject?.id ?? null,
@@ -1,24 +1,36 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { type ThemeColor } from '@ui/theme';
import { type ThemeColor, type ThemeType } from '@ui/theme';
import { isDefined } from 'twenty-shared/utils';
export type ColorSampleVariant = 'default' | 'pipeline';
export type ColorSampleProps = {
colorName: ThemeColor;
colorName?: ThemeColor;
color?: string;
variant?: ColorSampleVariant;
};
const getColor = (theme: ThemeType, colorName?: ThemeColor, color?: string) => {
if (isDefined(color)) {
return color;
}
if (isDefined(colorName)) {
return theme.tag.background[colorName];
}
return 'transparent';
};
const StyledColorSample = styled.div<ColorSampleProps>`
background-color: ${({ theme, colorName }) =>
theme.tag.background[colorName]};
border: 1px solid ${({ theme, colorName }) => theme.tag.text[colorName]};
background-color: ${({ theme, colorName, color }) =>
getColor(theme, colorName, color)};
border: 1px solid ${({ theme }) => theme.border.color.transparentStrong};
border-radius: 60px;
height: ${({ theme }) => theme.spacing(4)};
width: ${({ theme }) => theme.spacing(3)};
${({ colorName, theme, variant }) => {
${({ colorName, color, theme, variant }) => {
if (variant === 'pipeline')
return css`
align-items: center;
@@ -27,7 +39,7 @@ const StyledColorSample = styled.div<ColorSampleProps>`
justify-content: center;
&:after {
background-color: ${theme.tag.text[colorName]};
background-color: ${getColor(theme, colorName, color)};
border-radius: ${theme.border.radius.rounded};
content: '';
display: block;
@@ -1,3 +1,4 @@
import { RGBA } from '@ui/theme/constants/Rgba';
import { BORDER_COMMON } from './BorderCommon';
import { COLOR } from './Colors';
import { GRAY_SCALE } from './GrayScale';
@@ -11,6 +12,7 @@ export const BORDER_DARK = {
inverted: GRAY_SCALE.gray20,
danger: COLOR.red70,
blue: COLOR.blue30,
transparentStrong: RGBA(GRAY_SCALE.gray100, 0.16),
},
...BORDER_COMMON,
};
@@ -1,3 +1,4 @@
import { RGBA } from '@ui/theme/constants/Rgba';
import { BORDER_COMMON } from './BorderCommon';
import { COLOR } from './Colors';
import { GRAY_SCALE } from './GrayScale';
@@ -11,6 +12,7 @@ export const BORDER_LIGHT = {
inverted: GRAY_SCALE.gray60,
danger: COLOR.red20,
blue: COLOR.blue30,
transparentStrong: RGBA(GRAY_SCALE.gray100, 0.16),
},
...BORDER_COMMON,
};