647c32ff3e
## Summary
- **Eliminate `ICON_SIZES` / `ICON_STROKES` constants**: all icon
dimensions are now resolved at runtime via
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.X)`, ensuring
values always come from computed CSS variables
- **No more consumer imports from `twenty-ui/theme`**: moved
`ColorSchemeContext`, `ColorSchemeProvider`, `ThemeColor`,
`MAIN_COLOR_NAMES`, `getNextThemeColor`, `AnimationDuration` to
`twenty-ui/theme-constants`
- **Remove `ThemeContext` / `ThemeContextProvider` / `ThemeProvider` /
`ThemeType`**: replaced across ~300 files with `themeCssVariables` (for
CSS contexts) or `resolveThemeVariable` / `resolveThemeVariableAsNumber`
(for JS runtime values)
- **Simplify provider chain**: only `ColorSchemeProvider` remains — it
toggles `light`/`dark` class on `document.documentElement` and provides
`colorScheme` via React context
- **Fix pre-existing test failures**: `useIcons.test.ts`
(non-configurable ES module spy) and
`turnRecordFilterGroupIntoGqlOperationFilter.test.ts`
(`Omit<RecordFilter, 'id'>` type mismatch)
### Theme access pattern (before → after)
| Context | Before | After |
|---------|--------|-------|
| CSS (Linaria) | `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| JS runtime (icon size, animation) | `theme.icon.size.md` /
`ICON_SIZES.md` |
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.md)` |
| Color scheme check | `theme.name === 'dark'` |
`useContext(ColorSchemeContext).colorScheme === 'dark'` |
174 lines
4.5 KiB
TypeScript
174 lines
4.5 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { type ReactNode, useRef, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import { isNonEmptyString } from '@sniptt/guards';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { themeCssVariables } from '@ui/theme-constants';
|
|
import { AppTooltip, TooltipDelay } from './AppTooltip';
|
|
|
|
const spacing4 = themeCssVariables.spacing[4];
|
|
|
|
const StyledOverflowingMultilineText = styled.div<{
|
|
isContentOverflowing: boolean;
|
|
size: 'large' | 'small';
|
|
displayedMaxRows: number;
|
|
}>`
|
|
cursor: ${({ isContentOverflowing }) =>
|
|
isContentOverflowing ? 'pointer' : 'inherit'};
|
|
font-family: inherit;
|
|
font-size: inherit;
|
|
|
|
font-weight: inherit;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-decoration: inherit;
|
|
|
|
text-overflow: ellipsis;
|
|
height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')};
|
|
|
|
-webkit-line-clamp: ${({ displayedMaxRows }) =>
|
|
displayedMaxRows ? displayedMaxRows.toString() : '1'};
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
white-space: pre-wrap;
|
|
`;
|
|
|
|
const StyledOverflowingText = styled.div<{
|
|
isContentOverflowing: boolean;
|
|
size: 'large' | 'small';
|
|
}>`
|
|
cursor: ${({ isContentOverflowing }) =>
|
|
isContentOverflowing ? 'pointer' : 'inherit'};
|
|
font-family: inherit;
|
|
font-size: inherit;
|
|
|
|
font-weight: inherit;
|
|
max-width: 100%;
|
|
text-decoration: inherit;
|
|
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')};
|
|
|
|
white-space: nowrap;
|
|
`;
|
|
|
|
const StyledPre = styled.pre`
|
|
font-family: inherit;
|
|
white-space: pre-wrap;
|
|
`;
|
|
|
|
type OverflowingTextWithTooltipProps = {
|
|
size?: 'large' | 'small';
|
|
isTooltipMultiline?: boolean;
|
|
displayedMaxRows?: number;
|
|
tooltipDelay?: TooltipDelay;
|
|
} & (
|
|
| {
|
|
text: string | null | undefined;
|
|
tooltipContent?: never;
|
|
}
|
|
| {
|
|
text: Exclude<ReactNode, string | null | undefined>;
|
|
tooltipContent: string;
|
|
}
|
|
);
|
|
|
|
export const OverflowingTextWithTooltip = ({
|
|
size = 'small',
|
|
text,
|
|
isTooltipMultiline,
|
|
displayedMaxRows,
|
|
tooltipContent,
|
|
tooltipDelay = TooltipDelay.mediumDelay,
|
|
}: OverflowingTextWithTooltipProps) => {
|
|
const textElementId = `title-id-${+new Date()}`;
|
|
|
|
const textRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [isTitleOverflowing, setIsTitleOverflowing] = useState(false);
|
|
const [shouldRenderTooltip, setShouldRenderTooltip] = useState(false);
|
|
|
|
const handleMouseEnter = () => {
|
|
const isOverflowing = textRef.current
|
|
? textRef.current?.scrollHeight > textRef.current?.clientHeight ||
|
|
textRef.current.scrollWidth > textRef.current.clientWidth
|
|
: false;
|
|
|
|
setIsTitleOverflowing(isOverflowing);
|
|
setShouldRenderTooltip(true);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
setIsTitleOverflowing(false);
|
|
setShouldRenderTooltip(false);
|
|
};
|
|
|
|
const handleTooltipClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
};
|
|
|
|
const tooltipText = isNonEmptyString(tooltipContent)
|
|
? tooltipContent
|
|
: isNonEmptyString(text)
|
|
? text
|
|
: null;
|
|
|
|
return (
|
|
<>
|
|
{isDefined(displayedMaxRows) ? (
|
|
<StyledOverflowingMultilineText
|
|
data-testid="tooltip"
|
|
isContentOverflowing={isTitleOverflowing}
|
|
size={size}
|
|
displayedMaxRows={displayedMaxRows}
|
|
ref={textRef}
|
|
id={textElementId}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
{text}
|
|
</StyledOverflowingMultilineText>
|
|
) : (
|
|
<StyledOverflowingText
|
|
data-testid="tooltip"
|
|
isContentOverflowing={isTitleOverflowing}
|
|
size={size}
|
|
ref={textRef}
|
|
id={textElementId}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
{text}
|
|
</StyledOverflowingText>
|
|
)}
|
|
|
|
{shouldRenderTooltip &&
|
|
isTitleOverflowing &&
|
|
isDefined(tooltipText) &&
|
|
createPortal(
|
|
<div onClick={handleTooltipClick}>
|
|
<AppTooltip
|
|
anchorSelect={`#${textElementId}`}
|
|
offset={5}
|
|
noArrow
|
|
place="bottom"
|
|
positionStrategy="absolute"
|
|
delay={tooltipDelay}
|
|
isOpen={true}
|
|
>
|
|
{isTooltipMultiline ? (
|
|
<StyledPre>{tooltipText}</StyledPre>
|
|
) : (
|
|
tooltipText
|
|
)}
|
|
</AppTooltip>
|
|
</div>,
|
|
document.body,
|
|
)}
|
|
</>
|
|
);
|
|
};
|