[DevXP] Improve Linaria pre-build speed (#18382)

## Summary

This PR improves Linaria/WYW pre-build speed and continues the migration
of `twenty-ui` components away from runtime `ThemeContext` reads toward
static CSS variables and theme constants.

### Linaria/WYW profiling plugin improvements (`twenty-shared`)

- **Babel JIT warmup**: added a `buildStart` warmup step that triggers
WYW's Babel JIT compilation before the real build starts, so the first
real file doesn't pay the cold-start penalty
- **`configResolved` hook**: detects dev vs prod mode and resolves the
correct warmup file path relative to `config.root`
- **Dev-only per-file logging**: slow file warnings are now gated behind
`isDevMode`, keeping production/CI build output clean
- **`closeBundle` summary**: moved the final top-slow-files report to
`closeBundle` for accurate end-of-build reporting
- **Removed noisy progress interval logging** in favor of the warmup log
+ final summary

### Migration from `ThemeContext` to static CSS variables / constants

Across `twenty-ui`, replaced runtime `useTheme()` reads with:
- `themeCssVariables` CSS custom properties (colors, spacing)
- Hard-coded design-system constants (`ICON.size.md` → `16`,
`ICON.stroke.sm` → `1.6`) so components no longer need a React context
at render time — enabling Linaria static extraction

**Components migrated:**
- `Button`, `AnimatedButton`, `LightButton`, `LightIconButton`,
`AnimatedLightIconButton`, `ButtonIcon`, `ButtonSoon`
- `ProgressBar` (Framer Motion width animation → CSS `transition`)
- `Info`, `HorizontalSeparator`, `LinkChip`
- `MenuPicker`, `MenuItemLeftContent`, `MenuItemIconWithGripSwap`,
`NavigationBarItem`
- `JsonArrow`, `JsonNestedNode`
- `ModalHeader`

### Other
- Added `aria-valuenow` to `ProgressBar` for accessibility
- `VisibilityHidden` component updated to inline accessibility styles
This commit is contained in:
Charles Bochet
2026-03-04 17:04:16 +01:00
committed by GitHub
parent be01a85d67
commit eda905f271
55 changed files with 691 additions and 650 deletions
@@ -1,15 +1,13 @@
import { type AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { motion } from 'framer-motion';
import { IconReload } from 'twenty-ui/display';
import { THEME_DARK } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from 'twenty-ui/theme-constants';
type AppRootErrorFallbackProps = AppErrorDisplayProps;
const StyledContainer = styled.div`
background: ${THEME_DARK.background.noisy};
background: ${themeCssVariables.background.noisy};
box-sizing: border-box;
display: flex;
height: 100vh;
@@ -27,7 +25,7 @@ const StyledPanel = styled.div`
width: 100%;
`;
const StyledEmptyContainer = styled(motion.div)`
const StyledEmptyContainer = styled.div`
align-items: center;
width: 100%;
height: 100%;
@@ -94,8 +92,9 @@ const StyledButton = styled.button`
padding: 8px;
`;
const StyledIcon = styled(IconReload)`
const StyledIconContainer = styled.span`
color: ${themeCssVariables.grayScale.gray12};
display: inline-flex;
margin-right: 8px;
`;
@@ -124,7 +123,9 @@ export const AppRootErrorFallback = ({
</StyledEmptySubTitle>
</StyledEmptyTextContainer>
<StyledButton onClick={resetErrorBoundary}>
<StyledIcon size={16} />
<StyledIconContainer>
<IconReload size={ICON_SIZES.md} />
</StyledIconContainer>
{t`Reload`}
</StyledButton>
</StyledEmptyContainer>
@@ -12,7 +12,7 @@ const StyledItemsContainer = styled.div`
width: 100%;
`;
const StyledDropdownMenuSeparator = styled(DropdownMenuSeparator)`
const StyledSeparatorContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[1]};
margin-top: ${themeCssVariables.spacing[1]};
`;
@@ -59,7 +59,9 @@ export const FavoriteFolderPickerList = ({
/>
)}
{showNoFolderOption && filteredFolders.length > 0 && (
<StyledDropdownMenuSeparator />
<StyledSeparatorContainer>
<DropdownMenuSeparator />
</StyledSeparatorContainer>
)}
{filteredFolders.length > 0
? filteredFolders.map((folder) => (
@@ -17,13 +17,8 @@ import { RecordInlineCell } from '@/object-record/record-inline-cell/components/
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRecordCardBodyContainer = styled(RecordCardBodyContainer)`
padding: ${themeCssVariables.spacing[1]};
`;
type RecordCalendarCardBodyProps = {
recordId: string;
isRecordReadOnly: boolean;
@@ -73,7 +68,7 @@ export const RecordCalendarCardBody = ({
};
return (
<StyledRecordCardBodyContainer>
<RecordCardBodyContainer padding={themeCssVariables.spacing[1]}>
{visibleRecordFieldsExceptLabelIdentifier.map((recordField, index) => {
const correspondingFieldDefinition =
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
@@ -120,6 +115,6 @@ export const RecordCalendarCardBody = ({
</StopPropagationContainer>
);
})}
</StyledRecordCardBodyContainer>
</RecordCardBodyContainer>
);
};
@@ -9,12 +9,12 @@ import { useAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/us
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { isDefined } from 'twenty-shared/utils';
import { ChipVariant } from 'twenty-ui/components';
import { Checkbox, CheckboxVariant } from 'twenty-ui/input';
import { isRecordCalendarCardSelectedComponentFamilyState } from '@/object-record/record-calendar/record-calendar-card/states/isRecordCalendarCardSelectedComponentFamilyState';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCheckboxContainer = styled.div`
margin-left: auto;
@@ -27,10 +27,6 @@ const StyledRecordChipContainer = styled.div`
padding: ${themeCssVariables.spacing[1]};
`;
const StyledRecordCardHeaderContainer = styled(RecordCardHeaderContainer)`
padding: ${themeCssVariables.spacing[1]};
`;
type RecordCalendarCardHeaderProps = {
recordId: string;
};
@@ -68,7 +64,10 @@ export const RecordCalendarCardHeader = ({
}
return (
<StyledRecordCardHeaderContainer isCompact={isCompactModeActive}>
<RecordCardHeaderContainer
isCompact={isCompactModeActive}
padding={themeCssVariables.spacing[1]}
>
<StyledRecordChipContainer>
<StopPropagationContainer>
<RecordChip
@@ -93,6 +92,6 @@ export const RecordCalendarCardHeader = ({
/>
</StopPropagationContainer>
</StyledCheckboxContainer>
</StyledRecordCardHeaderContainer>
</RecordCardHeaderContainer>
);
};
@@ -1,13 +1,13 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCardBodyContainer = styled.div`
const StyledCardBodyContainer = styled.div<{ padding?: string }>`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing['0.5']};
padding-bottom: ${themeCssVariables.spacing[2]};
padding-left: 10px;
padding-right: ${themeCssVariables.spacing[2]};
padding: ${({ padding }) =>
padding ??
`0 ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} 10px`};
span {
align-items: center;
display: flex;
@@ -3,6 +3,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
export const StyledBoardCardHeaderContainer = styled.div<{
isCompact: boolean;
padding?: string;
}>`
align-items: center;
display: flex;
@@ -10,11 +11,9 @@ export const StyledBoardCardHeaderContainer = styled.div<{
justify-content: space-between;
font-weight: ${themeCssVariables.font.weight.medium};
height: 24px;
padding-bottom: ${({ isCompact }) =>
isCompact ? themeCssVariables.spacing[2] : themeCssVariables.spacing[1]};
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
padding-top: ${themeCssVariables.spacing[2]};
padding: ${({ padding, isCompact }) =>
padding ??
`${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} ${isCompact ? themeCssVariables.spacing[2] : themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]}`};
transition: padding ease-in-out 160ms;
img {
@@ -11,11 +11,11 @@ import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRecordChip = styled(RecordChip)`
const StyledRecordChipContainer = styled.div`
margin: ${themeCssVariables.spacing[2]};
`;
const StyledPlaceholder = styled(FormFieldPlaceholder)`
const StyledPlaceholderContainer = styled.div`
margin: ${themeCssVariables.spacing[2]};
`;
@@ -58,12 +58,18 @@ export const FormSingleRecordFieldChip = ({
if (!!draftValue && draftValue.type === 'static' && !!selectedRecord) {
return (
<StyledRecordChip
record={selectedRecord}
objectNameSingular={objectNameSingular}
/>
<StyledRecordChipContainer>
<RecordChip
record={selectedRecord}
objectNameSingular={objectNameSingular}
/>
</StyledRecordChipContainer>
);
}
return <StyledPlaceholder>{t`Select`}</StyledPlaceholder>;
return (
<StyledPlaceholderContainer>
<FormFieldPlaceholder>{t`Select`}</FormFieldPlaceholder>
</StyledPlaceholderContainer>
);
};
@@ -1,16 +1,8 @@
import { styled } from '@linaria/react';
import { useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId';
import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isDefined } from 'twenty-shared/utils';
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;
type MultipleRecordPickerMenuItemProps = {
recordId: string;
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
@@ -1,5 +1,4 @@
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
@@ -20,11 +19,6 @@ import { MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { type SearchRecord } from '~/generated/graphql';
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;
type MultipleRecordPickerMenuItemContentProps = {
searchRecord: SearchRecord;
objectMetadataItem: ObjectMetadataItem;
@@ -78,7 +72,7 @@ export const MultipleRecordPickerMenuItemContent = ({
multipleRecordPickerSearchableObjectMetadataItems.length > 1;
return (
<StyledSelectableItem
<SelectableListItem
itemId={searchRecord.recordId}
key={searchRecord.recordId}
onEnter={() => handleSelectChange(!isRecordSelectedWithObjectItem)}
@@ -103,6 +97,6 @@ export const MultipleRecordPickerMenuItemContent = ({
: undefined
}
/>
</StyledSelectableItem>
</SelectableListItem>
);
};
@@ -1,5 +1,3 @@
import { styled } from '@linaria/react';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
@@ -22,10 +20,6 @@ type SingleRecordPickerMenuItemProps = {
isRecordSelected: boolean;
};
const StyledSelectableItem = styled(SelectableListItem)`
width: 100%;
`;
export const SingleRecordPickerMenuItem = ({
morphItem,
onMorphItemSelected,
@@ -64,7 +58,7 @@ export const SingleRecordPickerMenuItem = ({
singleRecordPickerSearchableObjectMetadataItems.length > 1;
return (
<StyledSelectableItem
<SelectableListItem
itemId={morphItem.recordId}
key={morphItem.recordId}
onEnter={() => {
@@ -94,6 +88,6 @@ export const SingleRecordPickerMenuItem = ({
: undefined
}
/>
</StyledSelectableItem>
</SelectableListItem>
);
};
@@ -0,0 +1,21 @@
import { RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const HorizontalScrollBoxShadowCSS = `
&::after {
content: '';
position: absolute;
top: -1px;
height: calc(100% + 2px);
width: 4px;
right: -1px;
box-shadow:
2px 0px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px -4px 0px 0px);
visibility: var(
${RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
}
`;
@@ -9,51 +9,15 @@ import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object
import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnLastEmptyColumnWidthVariableName';
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthClassName';
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthVariableName';
import { RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName';
import { RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { HorizontalScrollBoxShadowCSS } from '@/object-record/record-table/components/HorizontalScrollBoxShadowCSS';
import { VerticalScrollBoxShadowCSS } from '@/object-record/record-table/components/VerticalScrollBoxShadowCSS';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
import { getRecordTableColumnFieldWidthCSSVariableName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthCSSVariableName';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const VerticalScrollBoxShadowCSS = `
&::before {
bottom: -1px;
box-shadow:
0px 2px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px 0px -4px 0px);
content: '';
height: 4px;
position: absolute;
visibility: var(
${RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
width: 100%;
}
`;
export const HorizontalScrollBoxShadowCSS = `
&::after {
content: '';
position: absolute;
top: -1px;
height: calc(100% + 2px);
width: 4px;
right: -1px;
box-shadow:
2px 0px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px -4px 0px 0px);
visibility: var(
${RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
}
`;
export { HorizontalScrollBoxShadowCSS, VerticalScrollBoxShadowCSS };
const MAX_COLUMNS = 100;
@@ -0,0 +1,20 @@
import { RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const VerticalScrollBoxShadowCSS = `
&::before {
bottom: -1px;
box-shadow:
0px 2px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px 0px -4px 0px);
content: '';
height: 4px;
position: absolute;
visibility: var(
${RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
width: 100%;
}
`;
@@ -1,7 +1,5 @@
import {
getRecordTableColumnWidthInlineStyles,
HorizontalScrollBoxShadowCSS,
} from '@/object-record/record-table/components/RecordTableStyleWrapper';
import { getRecordTableColumnWidthInlineStyles } from '@/object-record/record-table/components/RecordTableStyleWrapper';
import { HorizontalScrollBoxShadowCSS } from '@/object-record/record-table/components/HorizontalScrollBoxShadowCSS';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidthClassName';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
@@ -34,10 +32,9 @@ import {
type DraggableRubric,
type DraggableStateSnapshot,
} from '@hello-pangea/dnd';
import { useContext, useMemo } from 'react';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme-constants';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
const MAX_COLUMNS = 100;
@@ -139,8 +136,6 @@ export const RecordTableBodyVirtualizedDraggableClone = ({
}) => {
const realIndex = rubric.source.index;
const { theme } = useContext(ThemeContext);
const recordId = useAtomComponentFamilySelectorValue(
recordIdByRealIndexComponentFamilySelector,
realIndex,
@@ -178,10 +173,10 @@ export const RecordTableBodyVirtualizedDraggableClone = ({
style={{
...draggableProvided.draggableProps.style,
background: draggableSnapshot.isDragging
? theme.background.transparent.light
? themeCssVariables.background.transparent.light
: undefined,
borderColor: draggableSnapshot.isDragging
? `${theme.border.color.medium}`
? themeCssVariables.border.color.medium
: 'transparent',
opacity: isSecondaryDragged ? 0.3 : undefined,
}}
@@ -2,52 +2,10 @@ import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type IconComponent } from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCard = styled(Card)`
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
`;
const StyledTableRow = styled(TableRow)`
height: ${themeCssVariables.spacing[6]};
`;
const StyledTableCellLabel = styled(TableCell)<{
align?: 'left' | 'center' | 'right';
}>`
color: ${themeCssVariables.font.color.tertiary};
height: ${themeCssVariables.spacing[6]};
display: flex;
gap: ${themeCssVariables.spacing[2]};
justify-content: ${({ align }) =>
align === 'right'
? 'flex-end'
: align === 'center'
? 'center'
: 'flex-start'};
`;
const StyledTableCellValue = styled(TableCell)<{
align?: 'left' | 'center' | 'right';
clickable?: boolean;
}>`
color: ${themeCssVariables.font.color.primary};
cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')};
height: ${themeCssVariables.spacing[6]};
justify-content: ${({ align }) =>
align === 'left'
? 'flex-start'
: align === 'center'
? 'center'
: 'flex-end'};
`;
import { ICON_SIZES, themeCssVariables } from 'twenty-ui/theme-constants';
type TableItem = {
Icon?: IconComponent;
@@ -73,32 +31,42 @@ export const SettingsAdminTableCard = ({
valueAlign = 'left',
className,
}: SettingsAdminTableCardProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledCard rounded={rounded} className={className}>
<Card
rounded={rounded}
className={className}
backgroundColor={themeCssVariables.background.secondary}
>
<Table>
<TableBody>
{items.map((item, index) => (
<StyledTableRow
<TableRow
key={index + item.label}
gridAutoColumns={gridAutoColumns}
height={themeCssVariables.spacing[6]}
>
<StyledTableCellLabel align={labelAlign}>
{item.Icon && <item.Icon size={theme.icon.size.md} />}
<TableCell
align={labelAlign}
color={themeCssVariables.font.color.tertiary}
height={themeCssVariables.spacing[6]}
gap={themeCssVariables.spacing[2]}
>
{item.Icon && <item.Icon size={ICON_SIZES.md} />}
<span>{item.label}</span>
</StyledTableCellLabel>
<StyledTableCellValue
</TableCell>
<TableCell
align={valueAlign}
color={themeCssVariables.font.color.primary}
height={themeCssVariables.spacing[6]}
onClick={item.onClick}
clickable={isDefined(item.onClick)}
>
{item.value}
</StyledTableCellValue>
</StyledTableRow>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</StyledCard>
</Card>
);
};
@@ -61,42 +61,10 @@ const StyledPaginationContainer = styled.div`
padding: ${themeCssVariables.spacing[2]};
`;
const StyledTableCell = styled(TableCell)`
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledExpandableTableRow = styled(TableRow)<{ isExpanded: boolean }>`
cursor: pointer;
background-color: ${({ isExpanded }) =>
isExpanded
? themeCssVariables.background.transparent.light
: 'transparent'};
&:hover {
background-color: ${themeCssVariables.background.transparent.light};
}
`;
const StyledJobRowWrapper = styled.div`
display: contents;
`;
const StyledCheckboxCell = styled(TableCell)`
justify-content: center;
padding: 0;
padding-left: ${themeCssVariables.spacing[1]};
`;
const StyledHeaderCheckboxCell = styled(TableHeader)`
align-items: center;
display: flex;
justify-content: center;
padding-right: ${themeCssVariables.spacing[1]};
`;
const StyledButtonGroup = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[2]};
@@ -299,7 +267,10 @@ export const SettingsAdminQueueJobsTable = ({
<>
<Table>
<TableRow gridAutoColumns="32px 2fr 1fr 2fr 32px">
<StyledHeaderCheckboxCell>
<TableHeader
align="center"
padding={`0 ${themeCssVariables.spacing[1]} 0 ${themeCssVariables.spacing[2]}`}
>
{jobs.length > 0 && (
<Checkbox
checked={allJobsSelected}
@@ -307,7 +278,7 @@ export const SettingsAdminQueueJobsTable = ({
onChange={handleToggleAll}
/>
)}
</StyledHeaderCheckboxCell>
</TableHeader>
<TableHeader>{t`Job Name`}</TableHeader>
<TableHeader>{t`State`}</TableHeader>
<TableHeader align="right">{t`Timestamp`}</TableHeader>
@@ -320,22 +291,34 @@ export const SettingsAdminQueueJobsTable = ({
return (
<StyledJobRowWrapper key={job.id}>
<StyledExpandableTableRow
<TableRow
gridAutoColumns="32px 2fr 1fr 2fr 32px"
onClick={() => handleRowClick(job.id)}
isExpanded={isExpanded}
cursor="pointer"
hoverBackgroundColor={
themeCssVariables.background.transparent.light
}
>
<StyledCheckboxCell
<TableCell
align="center"
padding={`0 0 0 ${themeCssVariables.spacing[1]}`}
onClick={(e) => {
e.stopPropagation();
handleToggleJob(e, job.id);
}}
>
<Checkbox checked={isSelected} />
</StyledCheckboxCell>
<StyledTableCell title={job.name}>
</TableCell>
<TableCell
title={job.name}
maxWidth="200px"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
{job.name}
</StyledTableCell>
</TableCell>
<TableCell>
<SettingsAdminJobStateBadge
state={job.state}
@@ -364,7 +347,7 @@ export const SettingsAdminQueueJobsTable = ({
onDelete={() => handleDeleteOne(job.id)}
/>
</TableCell>
</StyledExpandableTableRow>
</TableRow>
<SettingsAdminJobDetailsExpandable
job={job}
isExpanded={isExpanded}
@@ -1,4 +1,3 @@
import { useContext } from 'react';
import { styled } from '@linaria/react';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
@@ -13,8 +12,11 @@ import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fiel
import { SettingsDataModelSetLabelIdentifierRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetLabelIdentifierRecordEffect';
import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue';
import { useIcons } from 'twenty-ui/display';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
ICON_SIZES,
ICON_STROKES,
themeCssVariables,
} from 'twenty-ui/theme-constants';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type SettingsDataModelFieldPreviewProps = {
@@ -59,7 +61,6 @@ export const SettingsDataModelFieldPreview = ({
shrink,
withFieldLabel = true,
}: SettingsDataModelFieldPreviewProps) => {
const { theme } = useContext(ThemeContext);
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular: objectNameSingular,
@@ -114,10 +115,7 @@ export const SettingsDataModelFieldPreview = ({
<StyledFieldPreview shrink={shrink}>
{!!withFieldLabel && (
<StyledFieldLabel>
<FieldIcon
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
<FieldIcon size={ICON_SIZES.md} stroke={ICON_STROKES.sm} />
{fieldMetadataItem.label}:
</StyledFieldLabel>
)}
@@ -4,21 +4,37 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
type TableCellProps = {
align?: 'left' | 'center' | 'right';
color?: string;
gap?: string;
height?: string;
maxWidth?: string;
minWidth?: string;
overflow?: string;
padding?: string;
textOverflow?: string;
whiteSpace?: string;
clickable?: boolean;
};
const StyledTableCell = styled.div<TableCellProps>`
align-items: center;
color: ${({ color }) => color || themeCssVariables.font.color.secondary};
cursor: ${({ clickable }) => (clickable === true ? 'pointer' : 'default')};
display: flex;
height: ${themeCssVariables.spacing[8]};
gap: ${({ gap }) => gap ?? 'normal'};
height: ${({ height }) => height ?? themeCssVariables.spacing[8]};
justify-content: ${({ align }) =>
align === 'right'
? 'flex-end'
: align === 'center'
? 'center'
: 'flex-start'};
padding: 0 ${themeCssVariables.spacing[2]};
max-width: ${({ maxWidth }) => maxWidth ?? 'none'};
min-width: ${({ minWidth }) => minWidth ?? 'auto'};
overflow: ${({ overflow }) => overflow ?? 'visible'};
padding: ${({ padding }) => padding ?? `0 ${themeCssVariables.spacing[2]}`};
text-align: ${({ align }) => align ?? 'left'};
text-overflow: ${({ textOverflow }) => textOverflow ?? 'clip'};
white-space: ${({ whiteSpace }) => whiteSpace ?? 'normal'};
`;
export { StyledTableCell as TableCell };
@@ -4,6 +4,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledTableHeader = styled.div<{
align?: 'left' | 'center' | 'right';
onClick?: () => void;
padding?: string;
}>`
gap: ${themeCssVariables.spacing[1]};
align-items: center;
@@ -18,7 +19,7 @@ const StyledTableHeader = styled.div<{
: align === 'center'
? 'center'
: 'flex-start'};
padding: 0 ${themeCssVariables.spacing[2]};
padding: ${({ padding }) => padding ?? `0 ${themeCssVariables.spacing[2]}`};
text-align: ${({ align }) => align ?? 'left'};
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
`;
@@ -4,15 +4,24 @@ import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledTableRow = styled.div<{
isSelected?: boolean;
isExpanded?: boolean;
onClick?: () => void;
to?: string;
gridAutoColumns?: string;
gridTemplateColumns?: string;
mobileGridAutoColumns?: string;
height?: string;
cursor?: string;
hoverBackgroundColor?: string;
}>`
background-color: ${({ isSelected }) =>
isSelected ? themeCssVariables.accent.quaternary : 'transparent'};
background-color: ${({ isSelected, isExpanded }) =>
isSelected
? themeCssVariables.accent.quaternary
: isExpanded === true
? themeCssVariables.background.transparent.light
: 'transparent'};
border-radius: ${themeCssVariables.border.radius.sm};
cursor: ${({ cursor }) => cursor ?? 'default'};
display: grid;
grid-auto-columns: ${({ gridAutoColumns }) => gridAutoColumns ?? '1fr'};
grid-template-columns: ${({ gridTemplateColumns }) =>
@@ -23,6 +32,7 @@ const StyledTableRow = styled.div<{
mobileGridAutoColumns ?? gridAutoColumns ?? '1fr'};
}
height: ${({ height }) => height ?? 'auto'};
grid-auto-flow: column;
transition: background-color
calc(${themeCssVariables.animation.duration.normal} * 1s);
@@ -30,11 +40,13 @@ const StyledTableRow = styled.div<{
text-decoration: none;
&:hover {
background-color: ${({ onClick, to }) =>
onClick || to
background-color: ${({ onClick, to, hoverBackgroundColor }) =>
hoverBackgroundColor ??
(onClick || to
? themeCssVariables.background.transparent.light
: 'transparent'};
cursor: ${({ onClick, to }) => (onClick || to ? 'pointer' : 'default')};
: 'transparent')};
cursor: ${({ onClick, to, cursor }) =>
cursor ?? (onClick || to ? 'pointer' : 'default')};
}
&[data-clickable='true'] {
@@ -44,6 +56,7 @@ const StyledTableRow = styled.div<{
type TableRowProps = {
isSelected?: boolean;
isExpanded?: boolean;
isClickable?: boolean;
onClick?: () => void;
to?: string;
@@ -52,10 +65,14 @@ type TableRowProps = {
gridAutoColumns?: string;
gridTemplateColumns?: string;
mobileGridAutoColumns?: string;
height?: string;
cursor?: string;
hoverBackgroundColor?: string;
};
export const TableRow = ({
isSelected,
isExpanded,
isClickable,
onClick,
to,
@@ -65,9 +82,13 @@ export const TableRow = ({
gridAutoColumns,
gridTemplateColumns,
mobileGridAutoColumns,
height,
cursor,
hoverBackgroundColor,
}: React.PropsWithChildren<TableRowProps>) => (
<StyledTableRow
isSelected={isSelected}
isExpanded={isExpanded}
onClick={onClick}
gridAutoColumns={gridAutoColumns}
gridTemplateColumns={gridTemplateColumns}
@@ -75,6 +96,9 @@ export const TableRow = ({
style={style}
data-clickable={isClickable}
mobileGridAutoColumns={mobileGridAutoColumns}
height={height}
cursor={cursor}
hoverBackgroundColor={hoverBackgroundColor}
to={to}
as={to ? Link : 'div'}
>
@@ -1,11 +1,18 @@
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { styled } from '@linaria/react';
const StyledWorkflowRunStepJsonContainer = styled(WorkflowStepBody)`
grid-template-rows: max-content;
gap: 0;
display: grid;
overflow: auto;
`;
const WorkflowRunStepJsonContainerInner = ({
children,
}: {
children: React.ReactNode;
}) => (
<WorkflowStepBody
display="grid"
gridTemplateRows="max-content"
rowGap="0"
overflow="auto"
>
{children}
</WorkflowStepBody>
);
export { StyledWorkflowRunStepJsonContainer as WorkflowRunStepJsonContainer };
export { WorkflowRunStepJsonContainerInner as WorkflowRunStepJsonContainer };
@@ -4,27 +4,54 @@ import { type AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplay
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledWorkflowStepBody = styled.div`
const StyledWorkflowStepBody = styled.div<{
rowGap?: string;
display?: string;
overflow?: string;
paddingBlock?: string;
paddingInline?: string;
gridTemplateRows?: string;
}>`
background: ${themeCssVariables.background.primary};
display: flex;
display: ${({ display }) => display ?? 'flex'};
flex: 1 1 auto;
flex-direction: column;
height: 100%;
overflow-y: scroll;
padding-block: ${themeCssVariables.spacing[4]};
padding-inline: ${themeCssVariables.spacing[3]};
row-gap: ${themeCssVariables.spacing[4]};
overflow: ${({ overflow }) => overflow ?? 'hidden scroll'};
padding-block: ${({ paddingBlock }) =>
paddingBlock ?? themeCssVariables.spacing[4]};
padding-inline: ${({ paddingInline }) =>
paddingInline ?? themeCssVariables.spacing[3]};
row-gap: ${({ rowGap }) => rowGap ?? themeCssVariables.spacing[4]};
grid-template-rows: ${({ gridTemplateRows }) => gridTemplateRows ?? 'none'};
`;
export const WorkflowStepBody = ({
children,
className,
rowGap,
display,
overflow,
paddingBlock,
paddingInline,
gridTemplateRows,
}: {
children: React.ReactNode;
className?: string;
rowGap?: string;
display?: string;
overflow?: string;
paddingBlock?: string;
paddingInline?: string;
gridTemplateRows?: string;
}) => {
return (
<StyledWorkflowStepBody className={className}>
<StyledWorkflowStepBody
rowGap={rowGap}
display={display}
overflow={overflow}
paddingBlock={paddingBlock}
paddingInline={paddingInline}
gridTemplateRows={gridTemplateRows}
>
<AppErrorBoundary
resetOnLocationChange={true}
FallbackComponent={({
@@ -55,11 +55,6 @@ const StyledTabList = styled(TabList)`
padding-left: ${themeCssVariables.spacing[2]};
`;
const StyledPermissionsStepBody = styled(WorkflowStepBody)`
padding-block: 0;
padding-inline: 0;
`;
export const WorkflowEditActionAiAgent = ({
action,
actionOptions,
@@ -281,14 +276,14 @@ export const WorkflowEditActionAiAgent = ({
behaveAsLinks={false}
/>
{currentTabId === WORKFLOW_AI_AGENT_TABS.PERMISSIONS ? (
<StyledPermissionsStepBody>
<WorkflowStepBody paddingBlock="0" paddingInline="0">
<WorkflowAiAgentPermissionsTab
action={action}
readonly={actionOptions.readonly === true}
isAgentLoading={agentLoading}
refetchAgent={refetchAgent}
/>
</StyledPermissionsStepBody>
</WorkflowStepBody>
) : (
<WorkflowStepBody>
<WorkflowAiAgentPromptTab
@@ -30,10 +30,6 @@ const StyledChildContainer = styled.div`
width: 100%;
`;
const StyledFilterBodyContainer = styled(WorkflowStepBody)`
gap: ${themeCssVariables.spacing[0]};
`;
type WorkflowEditActionFilterBodyProps = {
action: WorkflowFilterAction;
actionOptions:
@@ -84,7 +80,7 @@ export const WorkflowEditActionFilterBody = ({
onFilterSettingsUpdate,
}}
>
<StyledFilterBodyContainer>
<WorkflowStepBody rowGap={themeCssVariables.spacing[0]}>
<InputLabel>{t`Conditions`}</InputLabel>
{isDefined(rootStepFilterGroup) ? (
<StyledContainer>
@@ -119,7 +115,7 @@ export const WorkflowEditActionFilterBody = ({
) : (
<WorkflowStepFilterAddRootStepFilterButton />
)}
</StyledFilterBodyContainer>
</WorkflowStepBody>
</WorkflowStepFilterContext.Provider>
);
};
@@ -18,7 +18,7 @@ import { styled } from '@linaria/react';
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -32,8 +32,7 @@ import {
import { LightIconButton } from 'twenty-ui/input';
import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from 'twenty-ui/theme-constants';
export type WorkflowEditActionFormBuilderProps = {
triggerType: WorkflowTriggerType | undefined;
@@ -50,12 +49,6 @@ export type WorkflowEditActionFormBuilderProps = {
type FormData = WorkflowFormActionField[];
const StyledWorkflowStepBody = styled(WorkflowStepBody)`
display: block;
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
`;
const StyledFormFieldContainer = styled.div`
align-items: flex-end;
column-gap: ${themeCssVariables.spacing[1]};
@@ -74,17 +67,21 @@ const StyledDraggingIndicator = styled.div`
background-color: ${themeCssVariables.background.transparent.light};
`;
const StyledLightGripIconButton = styled(LightIconButton)`
const StyledGripButtonContainer = styled.div`
align-items: flex-end;
display: flex;
grid-area: grip;
margin-bottom: ${themeCssVariables.spacing[1]};
`;
const StyledLightTrashIconButton = styled(LightIconButton)`
const StyledTrashButtonContainer = styled.div`
align-items: flex-end;
display: flex;
grid-area: delete;
margin-bottom: ${themeCssVariables.spacing[1]};
`;
const StyledFormFieldInputContainer = styled(FormFieldInputContainer)`
const StyledFormFieldInputContainerWrapper = styled.div`
grid-area: input;
`;
@@ -115,7 +112,7 @@ const StyledFieldContainer = styled.div<{
}
`;
const StyledPlaceholder = styled(FormFieldPlaceholder)`
const StyledPlaceholderContainer = styled.div`
width: 100%;
`;
@@ -151,7 +148,6 @@ export const WorkflowEditActionFormBuilder = ({
action,
actionOptions,
}: WorkflowEditActionFormBuilderProps) => {
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const [formData, setFormData] = useState<FormData>(action.settings.input);
@@ -233,7 +229,10 @@ export const WorkflowEditActionFormBuilder = ({
return (
<>
<StyledWorkflowStepBody>
<WorkflowStepBody
display="block"
paddingInline={themeCssVariables.spacing[2]}
>
{triggerType && triggerType !== 'MANUAL' && isCalloutVisible && (
<StyledCalloutContainer>
<Callout
@@ -296,13 +295,15 @@ export const WorkflowEditActionFormBuilder = ({
{isDragging && <StyledDraggingIndicator />}
{showButtons && (
<StyledLightGripIconButton
Icon={IconGripVertical}
aria-label={t`Reorder field`}
/>
<StyledGripButtonContainer>
<LightIconButton
Icon={IconGripVertical}
aria-label={t`Reorder field`}
/>
</StyledGripButtonContainer>
)}
<StyledFormFieldInputContainer>
<StyledFormFieldInputContainerWrapper>
<InputLabel>{field.label || ''}</InputLabel>
<FormFieldInputRowContainer>
@@ -316,44 +317,51 @@ export const WorkflowEditActionFormBuilder = ({
<StyledFieldContainer
readonly={actionOptions.readonly}
>
<StyledPlaceholder>
{isDefined(field.placeholder) &&
isNonEmptyString(field.placeholder)
? field.placeholder
: getDefaultFormFieldSettings(field.type)
.placeholder}
</StyledPlaceholder>
<StyledPlaceholderContainer>
<FormFieldPlaceholder>
{isDefined(field.placeholder) &&
isNonEmptyString(field.placeholder)
? field.placeholder
: getDefaultFormFieldSettings(field.type)
.placeholder}
</FormFieldPlaceholder>
</StyledPlaceholderContainer>
{field.type === 'RECORD' && (
<IconChevronDown
size={theme.icon.size.md}
color={theme.font.color.tertiary}
size={ICON_SIZES.md}
color={
themeCssVariables.font.color.tertiary
}
/>
)}
</StyledFieldContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
</StyledFormFieldInputContainer>
</StyledFormFieldInputContainerWrapper>
{showButtons && (
<StyledLightTrashIconButton
Icon={IconTrash}
aria-label={t`Delete field`}
onClick={() => {
const updatedFormData = formData.filter(
(currentField) => currentField.id !== field.id,
);
<StyledTrashButtonContainer>
<LightIconButton
Icon={IconTrash}
aria-label={t`Delete field`}
onClick={() => {
const updatedFormData = formData.filter(
(currentField) =>
currentField.id !== field.id,
);
setFormData(updatedFormData);
setFormData(updatedFormData);
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: updatedFormData,
},
});
}}
/>
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: updatedFormData,
},
});
}}
/>
</StyledTrashButtonContainer>
)}
{isFieldSelected(field.id) && (
@@ -410,7 +418,7 @@ export const WorkflowEditActionFormBuilder = ({
>
<StyledFieldContainer>
<StyledAddFieldButtonContentContainer>
<IconPlus size={theme.icon.size.sm} />
<IconPlus size={ICON_SIZES.sm} />
{t`Add Field`}
</StyledAddFieldButtonContentContainer>
</StyledFieldContainer>
@@ -419,7 +427,7 @@ export const WorkflowEditActionFormBuilder = ({
</FormFieldInputContainer>
</StyledAddFieldButtonContainer>
)}
</StyledWorkflowStepBody>
</WorkflowStepBody>
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
</>
);
@@ -38,10 +38,6 @@ const StyledContainer = styled.div`
gap: ${themeCssVariables.spacing[4]};
`;
const StyledBodyContainer = styled(WorkflowStepBody)`
gap: ${themeCssVariables.spacing[2]};
`;
type WorkflowEditActionIfElseBodyProps = {
action: WorkflowIfElseAction;
actionOptions:
@@ -250,7 +246,7 @@ export const WorkflowEditActionIfElseBody = ({
};
return (
<StyledBodyContainer>
<WorkflowStepBody rowGap={themeCssVariables.spacing[2]}>
<InputLabel>{t`Conditions`}</InputLabel>
<StyledContainer>
{branches.map((branch, branchIndex) => {
@@ -303,6 +299,6 @@ export const WorkflowEditActionIfElseBody = ({
);
})}
</StyledContainer>
</StyledBodyContainer>
</WorkflowStepBody>
);
};
@@ -32,10 +32,6 @@ const StyledSearchContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledSearchInput = styled(SettingsTextInput)`
width: 100%;
`;
export const SettingsAIModelsTab = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const [currentWorkspace, setCurrentWorkspace] = useAtomState(
@@ -358,12 +354,13 @@ export const SettingsAIModelsTab = () => {
/>
<StyledSearchContainer>
<StyledSearchInput
<SettingsTextInput
instanceId="model-table-search"
LeftIcon={IconSearch}
placeholder={t`Search a model...`}
value={searchQuery}
onChange={setSearchQuery}
fullWidth
/>
</StyledSearchContainer>
@@ -5,7 +5,7 @@ import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useIcons, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from 'twenty-ui/theme-constants';
import { type Skill } from '~/generated-metadata/graphql';
@@ -15,28 +15,12 @@ export type SettingsSkillTableRowProps = {
link?: string;
};
export const StyledSkillTableRow = styled(TableRow)`
grid-template-columns: 1fr 120px 36px;
`;
const StyledNameTableCell = styled(TableCell)`
color: ${themeCssVariables.font.color.primary};
gap: ${themeCssVariables.spacing[2]};
min-width: 0;
overflow: hidden;
`;
const StyledIconContainer = styled.div`
align-items: center;
display: flex;
flex-shrink: 0;
`;
const StyledActionTableCell = styled(TableCell)`
justify-content: flex-end;
padding-right: ${themeCssVariables.spacing[2]};
`;
export const SettingsSkillTableRow = ({
skill,
action,
@@ -46,21 +30,27 @@ export const SettingsSkillTableRow = ({
const Icon = getIcon(skill.icon ?? 'IconSparkles');
return (
<StyledSkillTableRow
<TableRow
key={skill.id}
to={link}
gridTemplateColumns="1fr 120px 36px"
style={{ opacity: skill.isActive ? 1 : 0.5 }}
>
<StyledNameTableCell>
<TableCell
color={themeCssVariables.font.color.primary}
gap={themeCssVariables.spacing[2]}
minWidth="0"
overflow="hidden"
>
<StyledIconContainer>
<Icon size={16} />
<Icon size={ICON_SIZES.md} />
</StyledIconContainer>
<OverflowingTextWithTooltip text={skill.label} />
</StyledNameTableCell>
</TableCell>
<TableCell>
<SettingsItemTypeTag item={skill} />
</TableCell>
<StyledActionTableCell>{action}</StyledActionTableCell>
</StyledSkillTableRow>
<TableCell align="right">{action}</TableCell>
</TableRow>
);
};
@@ -1,6 +1,6 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import Skeleton from 'react-loading-skeleton';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -11,6 +11,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -23,8 +24,11 @@ import {
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { MenuItemToggle, UndecoratedLink } from 'twenty-ui/navigation';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
ICON_SIZES,
ICON_STROKES,
themeCssVariables,
} from 'twenty-ui/theme-constants';
import {
useActivateSkillMutation,
@@ -34,10 +38,7 @@ import {
import { SettingsSkillInactiveMenuDropDown } from '~/pages/settings/ai/components/SettingsSkillInactiveMenuDropDown';
import { SETTINGS_SKILL_TABLE_METADATA } from '~/pages/settings/ai/constants/SettingsSkillTableMetadata';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
import {
SettingsSkillTableRow,
StyledSkillTableRow,
} from './SettingsSkillTableRow';
import { SettingsSkillTableRow } from './SettingsSkillTableRow';
const StyledSearchAndFilterContainer = styled.div`
display: flex;
@@ -46,12 +47,11 @@ const StyledSearchAndFilterContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledSearchInput = styled(SettingsTextInput)`
const StyledSearchInputWrapper = styled.div`
flex: 1;
width: 100%;
`;
const StyledTableHeaderRow = styled(StyledSkillTableRow)`
const StyledTableHeaderRowContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[2]};
`;
@@ -68,7 +68,6 @@ export const SettingsSkillsTable = () => {
const [deleteSkill] = useDeleteSkillMutation();
const { t } = useLingui();
const { theme } = useContext(ThemeContext);
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [searchTerm, setSearchTerm] = useState('');
const [showDeactivated, setShowDeactivated] = useState(true);
@@ -123,13 +122,16 @@ export const SettingsSkillsTable = () => {
return (
<>
<StyledSearchAndFilterContainer>
<StyledSearchInput
instanceId="skill-table-search"
LeftIcon={IconSearch}
placeholder={t`Search a skill...`}
value={searchTerm}
onChange={setSearchTerm}
/>
<StyledSearchInputWrapper>
<SettingsTextInput
instanceId="skill-table-search"
LeftIcon={IconSearch}
placeholder={t`Search a skill...`}
value={searchTerm}
onChange={setSearchTerm}
fullWidth
/>
</StyledSearchInputWrapper>
<Dropdown
dropdownId="settings-skills-filter-dropdown"
dropdownPlacement="bottom-end"
@@ -160,21 +162,23 @@ export const SettingsSkillsTable = () => {
</StyledSearchAndFilterContainer>
<Table>
<StyledTableHeaderRow>
{SETTINGS_SKILL_TABLE_METADATA.fields.map(
(settingsSkillTableMetadataField) => (
<SortableTableHeader
key={settingsSkillTableMetadataField.fieldName}
fieldName={settingsSkillTableMetadataField.fieldName}
label={t(settingsSkillTableMetadataField.fieldLabel)}
tableId={SETTINGS_SKILL_TABLE_METADATA.tableId}
align={settingsSkillTableMetadataField.align}
initialSort={SETTINGS_SKILL_TABLE_METADATA.initialSort}
/>
),
)}
<TableHeader />
</StyledTableHeaderRow>
<StyledTableHeaderRowContainer>
<TableRow gridTemplateColumns="1fr 120px 36px">
{SETTINGS_SKILL_TABLE_METADATA.fields.map(
(settingsSkillTableMetadataField) => (
<SortableTableHeader
key={settingsSkillTableMetadataField.fieldName}
fieldName={settingsSkillTableMetadataField.fieldName}
label={t(settingsSkillTableMetadataField.fieldLabel)}
tableId={SETTINGS_SKILL_TABLE_METADATA.tableId}
align={settingsSkillTableMetadataField.align}
initialSort={SETTINGS_SKILL_TABLE_METADATA.initialSort}
/>
),
)}
<TableHeader />
</TableRow>
</StyledTableHeaderRowContainer>
{showSkeleton
? Array.from({ length: 3 }).map((_, index) => (
<Skeleton height={32} borderRadius={4} key={index} />
@@ -186,8 +190,8 @@ export const SettingsSkillsTable = () => {
action={
skill.isActive ? (
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
size={ICON_SIZES.md}
stroke={ICON_STROKES.sm}
/>
) : (
<SettingsSkillInactiveMenuDropDown
@@ -1,10 +1,10 @@
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
import {
FieldMetadataDefaultValue,
FieldMetadataOptions,
FieldMetadataType,
FieldMetadataUniversalSettings,
RelationAndMorphRelationFieldMetadataType
type FieldMetadataDefaultValue,
type FieldMetadataOptions,
type FieldMetadataType,
type FieldMetadataUniversalSettings,
type RelationAndMorphRelationFieldMetadataType,
} from '@/types';
export type RegularFieldManifest<
@@ -3,33 +3,75 @@ import { type Plugin } from 'vite';
const LINARIA_IMPORT_RE = /@linaria/;
// Minimal Linaria code used to trigger WYW's Babel JIT compilation before
// the real build starts, so the first real file doesn't pay the cold-start cost.
// The ID must be inside the project root so WYW can resolve @linaria/react
// from node_modules. It is set in configResolved once config.root is known.
const WARMUP_CODE = `import { styled } from '@linaria/react';
const StyledDiv = styled.div\`color: red;\`;
`;
type WywProfilingOptions = {
slowThresholdMs?: number;
// Used only for dev-mode real-time slow-file alerts. Summary always uses 10x avg.
devSlowThresholdMs?: number;
topSlowFilesCount?: number;
progressIntervalFiles?: number;
warmupThresholdMs?: number;
};
export const createWywProfilingPlugin = (
wywPlugin: Plugin,
options?: WywProfilingOptions,
): Plugin => {
const slowThresholdMs = options?.slowThresholdMs ?? 50;
const devSlowThresholdMs = options?.devSlowThresholdMs ?? 200;
const topSlowFilesCount = options?.topSlowFilesCount ?? 10;
const progressIntervalFiles = options?.progressIntervalFiles ?? 50;
const warmupThresholdMs = options?.warmupThresholdMs ?? 500;
let totalMs = 0;
let fileCount = 0;
let skippedCount = 0;
const slowFiles: { id: string; ms: number }[] = [];
let isDevMode = false;
let warmupId = `${process.cwd()}/src/__wyw_warmup__.tsx`;
const allTransforms: { id: string; ms: number }[] = [];
const originalTransform = wywPlugin.transform;
console.log(
`[linaria/wyw] CSS pre-build profiling enabled (slow threshold: ${slowThresholdMs}ms)`,
);
return {
...wywPlugin,
enforce: 'pre' as const,
configResolved(config) {
isDevMode = config.command === 'serve';
warmupId = `${config.root}/src/__wyw_warmup__.tsx`;
if (typeof wywPlugin.configResolved === 'function') {
(wywPlugin.configResolved as Function).call(this, config);
}
},
async buildStart() {
console.log(`[linaria/wyw] Starting CSS pre-build`);
const warmupStart = performance.now();
try {
const warmupResult = (originalTransform as Function).call(
this,
WARMUP_CODE,
warmupId,
);
if (
warmupResult !== null &&
typeof warmupResult === 'object' &&
'then' in warmupResult
) {
await warmupResult;
}
} catch {
// Expected: fake file path causes module resolution errors, but
// Babel's JIT compilation is already triggered — that's all we need.
}
const warmupMs = performance.now() - warmupStart;
const warmupWarning = warmupMs > warmupThresholdMs ? ' ⚠️ slow' : '';
console.log(
`[linaria/wyw] Pre-warm: ${warmupMs.toFixed(0)}ms${warmupWarning}`,
);
},
transform(code: string, id: string, ...rest: unknown[]) {
if (!LINARIA_IMPORT_RE.test(code)) {
skippedCount++;
@@ -47,14 +89,11 @@ export const createWywProfilingPlugin = (
const handleTiming = (elapsed: number) => {
totalMs += elapsed;
fileCount++;
allTransforms.push({ id, ms: elapsed });
if (elapsed > slowThresholdMs) {
slowFiles.push({ id, ms: elapsed });
}
if (fileCount % progressIntervalFiles === 0) {
if (isDevMode && elapsed > devSlowThresholdMs) {
console.log(
`[linaria/wyw] CSS pre-build progress: ${fileCount} transformed, ${skippedCount} skipped, ${totalMs.toFixed(0)}ms total`,
`[linaria/wyw] slow: ${id.replace(process.cwd(), '')} ${elapsed.toFixed(0)}ms`,
);
}
};
@@ -69,18 +108,21 @@ export const createWywProfilingPlugin = (
handleTiming(performance.now() - start);
return result;
},
buildEnd() {
console.log('\n[linaria/wyw] ===== CSS PRE-BUILD TIMING SUMMARY =====');
closeBundle: () => {
const avg = fileCount > 0 ? totalMs / fileCount : 0;
const dynamicThreshold = Math.round(10 * avg);
const slowFiles = allTransforms.filter((f) => f.ms > dynamicThreshold);
console.log('\n[linaria/wyw] ===== CSS PRE-BUILD SUMMARY =====');
console.log(`[linaria/wyw] Files transformed: ${fileCount}`);
console.log(`[linaria/wyw] Files skipped (no @linaria): ${skippedCount}`);
console.log(`[linaria/wyw] Transform time: ${totalMs.toFixed(0)}ms`);
console.log(
`[linaria/wyw] Avg per transformed file: ${fileCount > 0 ? (totalMs / fileCount).toFixed(1) : 0}ms`,
`[linaria/wyw] Avg per transformed file: ${avg.toFixed(1)}ms`,
);
if (slowFiles.length > 0) {
console.log(
`[linaria/wyw] Slow CSS pre-build files (>${slowThresholdMs}ms):`,
`[linaria/wyw] Slow files (>10x avg = ${dynamicThreshold}ms):`,
);
slowFiles
.sort((a, b) => b.ms - a.ms)
@@ -91,7 +133,6 @@ export const createWywProfilingPlugin = (
),
);
}
console.log('[linaria/wyw] ==========================================\n');
},
};
@@ -20,6 +20,7 @@ const {
MOBILE_VIEWPORT,
THEME_LIGHT,
THEME_DARK,
ICON,
prepareThemeForRootCssVariableInjection,
} = require('../dist/theme.cjs');
@@ -106,6 +107,11 @@ writeFileSync(
// must be a static number rather than a var(--...) reference.
export const MOBILE_VIEWPORT = ${MOBILE_VIEWPORT};
// Numeric icon size/stroke constants for components that require pixel values
// (e.g. icon size props) rather than CSS variable strings.
export const ICON_SIZES = ${serializeObject(ICON.size)} as const;
export const ICON_STROKES = ${serializeObject(ICON.stroke)} as const;
export const THEME_LIGHT_CSS_VARIABLE_ENTRIES: [string, string][] = ${serializeTupleArray(lightEntries)};
`,
'utf-8',
@@ -1,8 +1,15 @@
import { styled } from '@linaria/react';
import { VISIBILITY_HIDDEN } from '@ui/accessibility/utils/visibility-hidden';
const StyledSpan = styled.span`
${VISIBILITY_HIDDEN}
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
`;
export const VisibilityHidden = ({
@@ -22,11 +22,14 @@ export type LinkChipProps = Omit<
target?: '_blank' | '_self';
};
const StyledLink = styled(Link)`
const StyledLinkContainer = styled.span`
display: inline-flex;
text-decoration: none;
min-width: 0;
vertical-align: middle;
& > a {
text-decoration: none;
}
`;
export const LinkChip = ({
@@ -55,32 +58,34 @@ export const LinkChip = ({
});
return (
<StyledLink
to={to}
onClick={(event) => {
event.stopPropagation();
onClickHandler(event);
}}
onMouseDown={onMouseDownHandler}
data-click-outside-id={LINK_CHIP_CLICK_OUTSIDE_ID}
target={target}
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
>
<Chip
size={size}
label={label}
isLabelHidden={isLabelHidden}
isBold={isBold}
clickable={true}
variant={variant}
leftComponent={leftComponent}
rightComponent={rightComponent}
rightComponentDivider={rightComponentDivider}
accent={accent}
className={className}
maxWidth={maxWidth}
emptyLabel={emptyLabel}
/>
</StyledLink>
<StyledLinkContainer>
<Link
to={to}
onClick={(event) => {
event.stopPropagation();
onClickHandler(event);
}}
onMouseDown={onMouseDownHandler}
data-click-outside-id={LINK_CHIP_CLICK_OUTSIDE_ID}
target={target}
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
>
<Chip
size={size}
label={label}
isLabelHidden={isLabelHidden}
isBold={isBold}
clickable={true}
variant={variant}
leftComponent={leftComponent}
rightComponent={rightComponent}
rightComponentDivider={rightComponentDivider}
accent={accent}
className={className}
maxWidth={maxWidth}
emptyLabel={emptyLabel}
/>
</Link>
</StyledLinkContainer>
);
};
@@ -1,10 +1,9 @@
import { styled } from '@linaria/react';
import { IconInfoCircle } from '@ui/display/icon/components/TablerIcons';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
import { Button } from '@ui/input/button/components/Button/Button';
import React, { useContext } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
export type InfoAccent = 'blue' | 'danger';
@@ -20,10 +19,10 @@ const StyledTextContainer = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledIconInfoCircle = styled(IconInfoCircle)`
flex-shrink: 0;
& > svg {
flex-shrink: 0;
}
`;
const StyledInfo = styled.div<Pick<InfoProps, 'accent'>>`
@@ -57,8 +56,10 @@ const StyledInfo = styled.div<Pick<InfoProps, 'accent'>>`
}};
`;
const StyledLink = styled(Link)`
text-decoration: none;
const StyledLinkContainer = styled.span`
& > a {
text-decoration: none;
}
`;
export const Info = ({
@@ -68,22 +69,23 @@ export const Info = ({
onClick,
to,
}: InfoProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledInfo accent={accent}>
<StyledTextContainer>
<StyledIconInfoCircle size={theme.icon.size.md} />
<IconInfoCircle size={ICON_SIZES.md} />
{text}
</StyledTextContainer>
{buttonTitle && to && (
<StyledLink to={to}>
<Button
title={buttonTitle}
size="small"
variant="secondary"
accent={accent}
/>
</StyledLink>
<StyledLinkContainer>
<Link to={to}>
<Button
title={buttonTitle}
size="small"
variant="secondary"
accent={accent}
/>
</Link>
</StyledLinkContainer>
)}
{buttonTitle && onClick && !to && (
<Button
@@ -1,10 +1,7 @@
import { styled } from '@linaria/react';
import { type JSX, useContext } from 'react';
import { type JSX } from 'react';
import { Label } from '@ui/display';
import { THEME_COMMON, ThemeContext } from '@ui/theme';
const spacing3 = THEME_COMMON.spacing(3);
const spacing2 = THEME_COMMON.spacing(2);
import { themeCssVariables } from '@ui/theme-constants';
type HorizontalSeparatorProps = {
visible?: boolean;
@@ -21,16 +18,20 @@ const StyledSeparator = styled.div<{
background-color: ${({ backgroundColor }) => backgroundColor};
height: ${({ visible }) => (visible ? '1px' : '0')};
flex-shrink: 0;
margin-bottom: ${({ noMargin }) => (noMargin ? '0' : spacing3)};
margin-top: ${({ noMargin }) => (noMargin ? '0' : spacing3)};
margin-bottom: ${({ noMargin }) =>
noMargin ? '0' : themeCssVariables.spacing[3]};
margin-top: ${({ noMargin }) =>
noMargin ? '0' : themeCssVariables.spacing[3]};
width: 100%;
`;
const StyledSeparatorContainer = styled.div<{ noMargin: boolean }>`
align-items: center;
display: flex;
margin-bottom: ${({ noMargin }) => (noMargin ? '0' : spacing3)};
margin-top: ${({ noMargin }) => (noMargin ? '0' : spacing3)};
margin-bottom: ${({ noMargin }) =>
noMargin ? '0' : themeCssVariables.spacing[3]};
margin-top: ${({ noMargin }) =>
noMargin ? '0' : themeCssVariables.spacing[3]};
width: 100%;
`;
@@ -44,7 +45,7 @@ const StyledLine = styled.div<{
`;
const StyledText = styled.span`
margin: 0 ${spacing2};
margin: 0 ${themeCssVariables.spacing[2]};
`;
export const HorizontalSeparator = ({
@@ -53,8 +54,7 @@ export const HorizontalSeparator = ({
noMargin = false,
color,
}: HorizontalSeparatorProps): JSX.Element => {
const { theme } = useContext(ThemeContext);
const borderColor = color ?? theme.border.color.medium;
const borderColor = color ?? themeCssVariables.border.color.medium;
return (
<>
@@ -1,7 +1,6 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from '@ui/theme-constants';
import { motion } from 'framer-motion';
import { useState } from 'react';
export type ProgressBarProps = {
value: number;
@@ -26,43 +25,41 @@ const StyledBar = styled.div<StyledBarProps>`
width: 100%;
`;
const StyledBarFillingBase = styled.div<{
const StyledBarFilling = styled.div<{
barColor?: string;
withBorderRadius?: boolean;
}>`
background-color: ${({ barColor }) =>
barColor ?? themeCssVariables.font.color.primary};
height: 100%;
border-radius: ${({ withBorderRadius }) =>
withBorderRadius ? themeCssVariables.border.radius.md : '0'};
height: 100%;
width: 100%;
`;
const StyledBarFilling = motion.create(StyledBarFillingBase);
export const ProgressBar = ({
value,
className,
barColor,
backgroundColor = 'none',
withBorderRadius = false,
}: ProgressBarProps) => {
const [initialValue] = useState(value);
return (
<StyledBar
className={className}
backgroundColor={backgroundColor}
withBorderRadius={withBorderRadius}
role="progressbar"
aria-valuenow={Math.ceil(value)}
}: ProgressBarProps) => (
<StyledBar
className={className}
backgroundColor={backgroundColor}
withBorderRadius={withBorderRadius}
role="progressbar"
aria-valuenow={Math.ceil(value)}
>
<motion.div
style={{ height: '100%' }}
animate={{ width: `${Math.ceil(value)}%` }}
transition={{ duration: 0.3, ease: 'linear' }}
>
<StyledBarFilling
initial={{ width: `${initialValue}%` }}
animate={{ width: `${value}%` }}
barColor={barColor}
transition={{ ease: 'linear' }}
withBorderRadius={withBorderRadius}
/>
</StyledBar>
);
};
</motion.div>
</StyledBar>
);
@@ -2,12 +2,11 @@ import { styled } from '@linaria/react';
import { useIsMobile } from '@ui/utilities';
import { getOsShortcutSeparator } from '@ui/utilities/device/getOsShortcutSeparator';
import { type MotionProps, motion } from 'framer-motion';
import React, { useContext, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Pill } from '@ui/components/Pill/Pill';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
import {
type ButtonAccent,
type ButtonPosition,
@@ -368,7 +367,8 @@ const StyledButton = styled.button<
}
`;
const StyledSoonPill = styled(Pill)`
const StyledSoonPillContainer = styled.span`
display: flex;
margin-left: auto;
`;
@@ -413,9 +413,9 @@ const StyledShortcutLabel = styled.div<{
font-weight: ${themeCssVariables.font.weight.medium};
`;
const StyledIconContainer = styled(motion.div)`
display: flex;
const StyledIconContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
`;
@@ -446,7 +446,6 @@ export const AnimatedButton = ({
dataGloballyPreventClickOutside,
soonLabel = 'Soon',
}: AnimatedButtonProps) => {
const { theme } = useContext(ThemeContext);
const isMobile = useIsMobile();
const isDisabled = soon || disabled;
@@ -491,13 +490,17 @@ export const AnimatedButton = ({
data-globally-prevent-click-outside={dataGloballyPreventClickOutside}
>
{Icon && (
<StyledIconContainer animate={animate} transition={transition}>
<Icon size={theme.icon.size.sm} />
<StyledIconContainer>
<motion.div animate={animate} transition={transition}>
<Icon size={ICON_SIZES.sm} />
</motion.div>
</StyledIconContainer>
)}
{animatedSvg && (
<StyledIconContainer animate={animate} transition={transition}>
{animatedSvg}
<StyledIconContainer>
<motion.div animate={animate} transition={transition}>
{animatedSvg}
</motion.div>
</StyledIconContainer>
)}
{title}
@@ -509,7 +512,11 @@ export const AnimatedButton = ({
</StyledShortcutLabel>
</>
)}
{soon && <StyledSoonPill label={soonLabel} />}
{soon && (
<StyledSoonPillContainer>
<Pill label={soonLabel} />
</StyledSoonPillContainer>
)}
</StyledButton>
);
};
@@ -4,10 +4,9 @@ import {
type LightIconButtonAccent,
type LightIconButtonSize,
} from '@ui/input/button/components/LightIconButton';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
import { motion, type MotionProps } from 'framer-motion';
import { type ComponentProps, type MouseEvent, useContext } from 'react';
import { type ComponentProps, type MouseEvent } from 'react';
export type AnimatedLightIconButtonProps = {
className?: string;
@@ -108,8 +107,6 @@ export const AnimatedLightIconButton = ({
onClick,
title,
}: AnimatedLightIconButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
data-testid={testId}
@@ -125,9 +122,7 @@ export const AnimatedLightIconButton = ({
>
<StyledIconContainer animate={animate} transition={transition}>
{Icon && (
<Icon
size={size === 'medium' ? theme.icon.size.md : theme.icon.size.sm}
/>
<Icon size={size === 'medium' ? ICON_SIZES.md : ICON_SIZES.sm} />
)}
</StyledIconContainer>
</StyledButton>
@@ -3,7 +3,6 @@ import { type IconComponent } from '@ui/display/icon/types/IconComponent';
import { ButtonHotkeys } from '@ui/input/button/components/Button/internal/ButtonHotKeys';
import { ButtonIcon } from '@ui/input/button/components/Button/internal/ButtonIcon';
import { ButtonSoon } from '@ui/input/button/components/Button/internal/ButtonSoon';
import { GRAY_SCALE_LIGHT } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { useIsMobile } from '@ui/utilities';
import { type ClickOutsideAttributes } from '@ui/utilities/types/ClickOutsideAttributes';
@@ -128,7 +127,7 @@ const computeButtonDynamicStyles = (
}`
: 'none';
result.color = !inverted
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: themeCssVariables.color.blue;
if (!disabled) {
result.hoverBackground = !inverted
@@ -191,10 +190,10 @@ const computeButtonDynamicStyles = (
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: themeCssVariables.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
@@ -232,10 +231,10 @@ const computeButtonDynamicStyles = (
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: themeCssVariables.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
@@ -273,10 +272,10 @@ const computeButtonDynamicStyles = (
: 'transparent'
: variant === 'secondary'
? focus || disabled
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: themeCssVariables.background.transparent.primary
: focus
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: 'transparent';
result.borderWidthOverride = '1px 1px 1px 1px';
result.boxShadow =
@@ -416,7 +415,7 @@ const computeButtonWrapperColor = (
: themeCssVariables.font.color.secondary;
case 'blue':
return !inverted
? GRAY_SCALE_LIGHT.gray1
? themeCssVariables.grayScale.gray1
: themeCssVariables.color.blue;
case 'danger':
return !inverted
@@ -2,9 +2,7 @@ import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { Loader } from '@ui/feedback';
import { baseTransitionTiming } from '@ui/input/button/components/Button/constant';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { useContext } from 'react';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
const StyledIcon = styled.div<{
isLoading: boolean;
@@ -43,20 +41,17 @@ export const ButtonIcon = ({
}: {
Icon?: IconComponent;
isLoading?: boolean;
}) => {
const { theme } = useContext(ThemeContext);
return (
<StyledIconWrapper>
{isLoading && (
<StyledLoader>
<Loader />
</StyledLoader>
)}
{Icon && (
<StyledIcon isLoading={!!isLoading}>
<Icon size={theme.icon.size.sm} />
</StyledIcon>
)}
</StyledIconWrapper>
);
};
}) => (
<StyledIconWrapper>
{isLoading && (
<StyledLoader>
<Loader />
</StyledLoader>
)}
{Icon && (
<StyledIcon isLoading={!!isLoading}>
<Icon size={ICON_SIZES.sm} />
</StyledIcon>
)}
</StyledIconWrapper>
);
@@ -1,7 +1,8 @@
import { styled } from '@linaria/react';
import { Pill } from '@ui/components';
const StyledSoonPill = styled(Pill)`
const StyledSoonPillContainer = styled.span`
display: flex;
margin-left: auto;
`;
@@ -10,5 +11,7 @@ type ButtonSoonProps = {
};
export const ButtonSoon = ({ label = 'Soon' }: ButtonSoonProps) => (
<StyledSoonPill label={label} />
<StyledSoonPillContainer>
<Pill label={label} />
</StyledSoonPillContainer>
);
@@ -1,8 +1,7 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { type MouseEvent, useContext } from 'react';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
import { type MouseEvent } from 'react';
export type LightButtonAccent = 'secondary' | 'tertiary';
@@ -90,8 +89,6 @@ export const LightButton = ({
type = 'button',
onClick,
}: LightButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
onClick={onClick}
@@ -102,7 +99,7 @@ export const LightButton = ({
className={className}
active={active}
>
{!!Icon && <Icon size={theme.icon.size.md} />}
{!!Icon && <Icon size={ICON_SIZES.md} />}
{title}
</StyledButton>
);
@@ -1,8 +1,7 @@
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { type ComponentProps, type MouseEvent, useContext } from 'react';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
import { type ComponentProps, type MouseEvent } from 'react';
export type LightIconButtonAccent = 'secondary' | 'tertiary';
export type LightIconButtonSize = 'small' | 'medium';
@@ -106,8 +105,6 @@ export const LightIconButton = ({
onClick,
title,
}: LightIconButtonProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledButton
data-testid={testId}
@@ -122,9 +119,7 @@ export const LightIconButton = ({
title={title}
>
{Icon && (
<Icon
size={size === 'medium' ? theme.icon.size.md : theme.icon.size.sm}
/>
<Icon size={size === 'medium' ? ICON_SIZES.md : ICON_SIZES.sm} />
)}
</StyledButton>
);
@@ -7,7 +7,6 @@ import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeV
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { ANIMATION } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
@@ -50,8 +49,6 @@ const StyledJsonListBase = styled.ul<{
}
`;
const StyledJsonList = motion.create(StyledJsonListBase);
export const JsonNestedNode = ({
label,
Icon,
@@ -80,45 +77,34 @@ export const JsonNestedNode = ({
);
const renderedChildren = (
<StyledJsonList
initial={{
height: 0,
opacity: 0,
overflowY: 'clip',
}}
animate={{
height: 'auto',
opacity: 1,
overflowY: 'clip',
}}
exit={{
height: 0,
opacity: 0,
overflowY: 'clip',
}}
transition={{ duration: ANIMATION.duration.normal }}
depth={depth}
<motion.div
initial={{ height: 0, opacity: 0, overflow: 'clip' }}
animate={{ height: 'auto', opacity: 1, overflow: 'clip' }}
exit={{ height: 0, opacity: 0, overflow: 'clip' }}
transition={{ duration: 0.3 }}
>
{elements.length === 0 ? (
<JsonNodeValue valueAsString={emptyElementsText} />
) : (
elements.map(({ id, label, value }) => {
const nextKeyPath = isNonEmptyString(keyPath)
? `${keyPath}.${id}`
: String(id);
<StyledJsonListBase depth={depth}>
{elements.length === 0 ? (
<JsonNodeValue valueAsString={emptyElementsText} />
) : (
elements.map(({ id, label, value }) => {
const nextKeyPath = isNonEmptyString(keyPath)
? `${keyPath}.${id}`
: String(id);
return (
<JsonNode
key={id}
label={label}
value={value}
depth={depth + 1}
keyPath={nextKeyPath}
/>
);
})
)}
</StyledJsonList>
return (
<JsonNode
key={id}
label={label}
value={value}
depth={depth + 1}
keyPath={nextKeyPath}
/>
);
})
)}
</StyledJsonListBase>
</motion.div>
);
const handleArrowClick = () => {
@@ -2,10 +2,8 @@ import { styled } from '@linaria/react';
import { VisibilityHidden } from '@ui/accessibility';
import { IconChevronDown } from '@ui/display';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { ANIMATION, ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
import { motion } from 'framer-motion';
import { useContext } from 'react';
const StyledButton = styled.button<{
variant?: 'blue' | 'red';
@@ -31,8 +29,6 @@ const StyledButton = styled.button<{
cursor: pointer;
`;
const MotionIconChevronDown = motion.create(IconChevronDown);
export const JsonArrow = ({
isOpen,
onClick,
@@ -42,30 +38,29 @@ export const JsonArrow = ({
onClick: () => void;
variant?: 'blue' | 'red';
}) => {
const { theme } = useContext(ThemeContext);
const { arrowButtonCollapsedLabel, arrowButtonExpandedLabel } =
useJsonTreeContextOrThrow();
const iconColor =
variant === 'blue'
? themeCssVariables.color.blue
: variant === 'red'
? themeCssVariables.font.color.danger
: themeCssVariables.font.color.secondary;
return (
<StyledButton variant={variant} onClick={onClick}>
<VisibilityHidden>
{isOpen ? arrowButtonExpandedLabel : arrowButtonCollapsedLabel}
</VisibilityHidden>
<MotionIconChevronDown
size={theme.icon.size.md}
color={
variant === 'blue'
? theme.color.blue
: variant === 'red'
? theme.font.color.danger
: theme.font.color.secondary
}
<motion.div
initial={false}
animate={{ rotate: isOpen ? 0 : -90 }}
transition={{ duration: ANIMATION.duration.normal }}
/>
transition={{ duration: 0.3 }}
>
<IconChevronDown size={ICON_SIZES.md} color={iconColor} />
</motion.div>
</StyledButton>
);
};
@@ -5,7 +5,10 @@ import { type ComponentPropsWithoutRef } from 'react';
const StyledCard = styled.div<{
fullWidth?: boolean;
rounded?: boolean;
backgroundColor?: string;
}>`
background-color: ${({ backgroundColor }) =>
backgroundColor ?? 'transparent'};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${({ rounded }) =>
rounded
@@ -19,6 +22,7 @@ const StyledCard = styled.div<{
type CardProps = ComponentPropsWithoutRef<'div'> & {
fullWidth?: boolean;
rounded?: boolean;
backgroundColor?: string;
};
export const Card = ({
@@ -26,6 +30,7 @@ export const Card = ({
className,
fullWidth,
rounded,
backgroundColor,
...rest
}: CardProps) => {
return (
@@ -33,6 +38,7 @@ export const Card = ({
className={className}
fullWidth={fullWidth}
rounded={rounded}
backgroundColor={backgroundColor}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
@@ -40,7 +40,6 @@ export type ModalHeaderProps = React.PropsWithChildren & {
hasBorderBottom?: boolean;
paddingHorizontal?: number;
backgroundColor?: string;
className?: string;
};
export const ModalHeader = ({
@@ -50,7 +49,6 @@ export const ModalHeader = ({
hasBorderBottom,
paddingHorizontal,
backgroundColor,
className,
}: ModalHeaderProps) => (
<StyledHeader
noPadding={noPadding}
@@ -58,7 +56,6 @@ export const ModalHeader = ({
hasBorderBottom={hasBorderBottom}
paddingHorizontal={paddingHorizontal}
backgroundColor={backgroundColor}
className={className}
>
{children}
</StyledHeader>
@@ -1,14 +1,16 @@
import { styled } from '@linaria/react';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import {
AppTooltip,
TooltipDelay,
TooltipPosition,
type IconComponent,
} from '@ui/display';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import {
ICON_SIZES,
ICON_STROKES,
themeCssVariables,
} from '@ui/theme-constants';
import { isDefined } from 'twenty-shared/utils';
const StyledMenuPicker = styled.button<{
@@ -135,8 +137,6 @@ export const MenuPicker = ({
tooltipDelay = TooltipDelay.noDelay,
tooltipOffset = 5,
}: MenuPickerProps) => {
const { theme } = useContext(ThemeContext);
return (
<>
<StyledMenuPicker
@@ -151,7 +151,7 @@ export const MenuPicker = ({
aria-label={label}
>
<StyledIconContainer>
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
<Icon size={ICON_SIZES.md} stroke={ICON_STROKES.sm} />
</StyledIconContainer>
{isDefined(label) && showLabel && (
@@ -1,9 +1,10 @@
import { useContext } from 'react';
import { styled } from '@linaria/react';
import { type IconComponent, IconGripVertical } from '@ui/display';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import {
ICON_SIZES,
ICON_STROKES,
themeCssVariables,
} from '@ui/theme-constants';
import { MenuItemIconBoxContainer } from './MenuItemIconBoxContainer';
const StyledIconSwapContainer = styled.div`
@@ -38,8 +39,6 @@ export const MenuItemIconWithGripSwap = ({
withIconContainer = false,
gripIconColor,
}: MenuItemIconWithGripSwapProps) => {
const { theme } = useContext(ThemeContext);
if (!LeftIcon) {
return null;
}
@@ -47,12 +46,12 @@ export const MenuItemIconWithGripSwap = ({
const iconContent = (
<StyledIconSwapContainer>
<StyledDefaultIcon className="grip-swap-default-icon">
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
<LeftIcon size={ICON_SIZES.md} stroke={ICON_STROKES.sm} />
</StyledDefaultIcon>
<StyledHoverIcon className="grip-swap-hover-icon">
<IconGripVertical
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
size={ICON_SIZES.md}
stroke={ICON_STROKES.sm}
color={gripIconColor}
/>
</StyledHoverIcon>
@@ -1,5 +1,5 @@
import { isNonEmptyString, isString } from '@sniptt/guards';
import { useContext, type ReactNode } from 'react';
import { type ReactNode } from 'react';
import { styled } from '@linaria/react';
import {
@@ -7,7 +7,11 @@ import {
IconGripVertical,
OverflowingTextWithTooltip,
} from '@ui/display';
import { ThemeContext } from '@ui/theme';
import {
ICON_SIZES,
ICON_STROKES,
themeCssVariables,
} from '@ui/theme-constants';
import { type MenuItemDraggableGripMode } from '../../types/MenuItemDraggableGripMode';
import { MenuItemIcon } from './MenuItemIcon';
import { MenuItemIconBoxContainer } from './MenuItemIconBoxContainer';
@@ -55,13 +59,11 @@ export const MenuItemLeftContent = ({
gripMode = 'never',
disabled = false,
}: MenuItemLeftContentProps) => {
const { theme } = useContext(ThemeContext);
const gripIconColor = withIconContainer
? theme.font.color.tertiary
? themeCssVariables.font.color.tertiary
: disabled
? theme.font.color.extraLight
: theme.font.color.light;
? themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.light;
return (
<StyledMenuItemLeftContent className={className}>
@@ -70,8 +72,8 @@ export const MenuItemLeftContent = ({
<MenuItemIconBoxContainer>
<StyledDraggableItem>
<IconGripVertical
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
size={ICON_SIZES.md}
stroke={ICON_STROKES.sm}
color={gripIconColor}
/>
</StyledDraggableItem>
@@ -79,8 +81,8 @@ export const MenuItemLeftContent = ({
) : (
<StyledDraggableItem>
<IconGripVertical
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
size={ICON_SIZES.md}
stroke={ICON_STROKES.sm}
color={gripIconColor}
/>
</StyledDraggableItem>
@@ -1,7 +1,5 @@
import { styled } from '@linaria/react';
import { isUndefined } from '@sniptt/guards';
import { IconCheck } from '@ui/display';
import { themeCssVariables } from '@ui/theme-constants';
import { type MenuItemAccent } from '../../types/MenuItemAccent';
@@ -45,7 +43,7 @@ export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
disabled || isHoverBackgroundDisabled ? 'none' : 'background 0.1s ease'};
color: ${({ accent, disabled }) => {
if (!isUndefined(disabled) && disabled !== false) {
if (disabled !== undefined && disabled !== false) {
return themeCssVariables.font.color.tertiary;
}
switch (accent) {
@@ -168,7 +166,7 @@ export const StyledHoverableMenuItemBase = styled(
}
cursor: ${({ cursor, disabled }) => {
if (!isUndefined(disabled) && disabled !== false) {
if (disabled !== undefined && disabled !== false) {
return 'default';
}
@@ -181,11 +179,19 @@ export const StyledHoverableMenuItemBase = styled(
}};
`;
export const StyledMenuItemIconCheck = styled(IconCheck)`
const StyledMenuItemIconCheckContainer = styled.div`
align-items: center;
display: flex;
flex-shrink: 0;
margin-right: ${themeCssVariables.spacing[1]};
`;
export const StyledMenuItemIconCheck = ({ size }: { size?: number }) => (
<StyledMenuItemIconCheckContainer>
<IconCheck size={size} />
</StyledMenuItemIconCheckContainer>
);
export const StyledMenuItemContextualText = styled.div`
color: ${themeCssVariables.font.color.light};
font-family: inherit;
@@ -1,9 +1,6 @@
import { useContext } from 'react';
import { styled } from '@linaria/react';
import { type IconComponent } from '@ui/display/icon/types/IconComponent';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
const StyledIconButton = styled.div<{ isActive?: boolean }>`
align-items: center;
@@ -33,12 +30,8 @@ export const NavigationBarItem = ({
Icon,
isActive,
onClick,
}: NavigationBarItemProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledIconButton isActive={isActive} onClick={onClick}>
<Icon color={theme.color.gray10} size={theme.icon.size.lg} />
</StyledIconButton>
);
};
}: NavigationBarItemProps) => (
<StyledIconButton isActive={isActive} onClick={onClick}>
<Icon color={themeCssVariables.grayScale.gray10} size={ICON_SIZES.lg} />
</StyledIconButton>
);
@@ -5,6 +5,20 @@
// must be a static number rather than a var(--...) reference.
export const MOBILE_VIEWPORT = 768;
// Numeric icon size/stroke constants for components that require pixel values
// (e.g. icon size props) rather than CSS variable strings.
export const ICON_SIZES = {
sm: 14,
md: 16,
lg: 20,
xl: 24,
} as const;
export const ICON_STROKES = {
sm: 1.6,
md: 2,
lg: 2.5,
} as const;
export const THEME_LIGHT_CSS_VARIABLE_ENTRIES: [string, string][] = [
['--t-icon-size-sm', '14'],
['--t-icon-size-md', '16'],
@@ -11,5 +11,7 @@ export { themeCssVariables } from './generated/themeCssVariables';
export { THEME_DARK_CSS_VARIABLE_ENTRIES } from './generated/themeDarkCssVariableEntries';
export {
MOBILE_VIEWPORT,
ICON_SIZES,
ICON_STROKES,
THEME_LIGHT_CSS_VARIABLE_ENTRIES,
} from './generated/themeLightCssVariableEntries';