Complete linaria migration (#18361)
## Summary
Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.
This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.
## Changes
### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals
### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support
### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection
### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales
## How it works
Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.
- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
This commit is contained in:
+9
-14
@@ -1,6 +1,8 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { IconPoint } from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
@@ -18,18 +20,11 @@ const StyledDotContainer = styled.div<{ dotPosition: DotPosition }>`
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: ${({ theme }) => theme.spacing(-5)};
|
||||
left: calc(-1 * ${themeCssVariables.spacing[5]});
|
||||
|
||||
${({ dotPosition }) => {
|
||||
if (dotPosition === 'top') {
|
||||
return `
|
||||
top: 0;
|
||||
`;
|
||||
}
|
||||
return `
|
||||
align-items: center;
|
||||
`;
|
||||
}}
|
||||
top: ${({ dotPosition }) => (dotPosition === 'top' ? '0' : 'auto')};
|
||||
align-items: ${({ dotPosition }) =>
|
||||
dotPosition === 'top' ? 'stretch' : 'center'};
|
||||
`;
|
||||
|
||||
const StyledIconPoint = styled(IconPoint)`
|
||||
@@ -41,7 +36,7 @@ export const AdvancedSettingsContentWrapperWithDot = ({
|
||||
hideDot = false,
|
||||
dotPosition = 'centered',
|
||||
}: AdvancedSettingsContentWrapperWithDotProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{!hideDot && (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AdvancedSettingsContentWrapperWithDot } from '@/settings/components/Adv
|
||||
import { ADVANCED_SETTINGS_ANIMATION_DURATION } from '@/settings/constants/AdvancedSettingsAnimationDurations';
|
||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
const StyledContent = styled.div`
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { CancelButton } from './CancelButton';
|
||||
import { SaveButton } from './SaveButton';
|
||||
@@ -7,7 +8,7 @@ import { SaveButton } from './SaveButton';
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
type SaveAndCancelButtonsProps = {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledHr = styled.hr`
|
||||
border: none;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
margin: 0px;
|
||||
margin-left: ${({ theme }) => theme.spacing(4)};
|
||||
margin-right: ${({ theme }) => theme.spacing(4)};
|
||||
margin-left: ${themeCssVariables.spacing[4]};
|
||||
margin-right: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
export const Separator = () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Card, CardContent } from 'twenty-ui/layout';
|
||||
import { IconChevronRight } from 'twenty-ui/display';
|
||||
import { Pill } from 'twenty-ui/components';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SettingsCardProps = {
|
||||
description?: string;
|
||||
@@ -22,21 +23,23 @@ const StyledCard = styled(Card)<{
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}>`
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
|
||||
color: ${({ disabled }) =>
|
||||
disabled
|
||||
? themeCssVariables.font.color.extraLight
|
||||
: themeCssVariables.font.color.tertiary};
|
||||
cursor: ${({ disabled, onClick }) =>
|
||||
disabled ? 'not-allowed' : onClick ? 'pointer' : 'default'};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled(CardContent)<object>`
|
||||
const StyledCardContent = styled(CardContent)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2, 2)};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.quaternary};
|
||||
background-color: ${themeCssVariables.background.quaternary};
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
@@ -44,26 +47,28 @@ const StyledCardContent = styled(CardContent)<object>`
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div<{ disabled?: boolean }>`
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? 'inherit' : theme.font.color.secondary};
|
||||
color: ${({ disabled }) =>
|
||||
disabled
|
||||
? themeCssVariables.font.color.extraLight
|
||||
: themeCssVariables.font.color.secondary};
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(7)};
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
padding-left: ${themeCssVariables.spacing[7]};
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
@@ -84,7 +89,7 @@ export const SettingsCard = ({
|
||||
className,
|
||||
Status,
|
||||
}: SettingsCardProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<StyledCard
|
||||
|
||||
+8
-6
@@ -3,19 +3,21 @@ import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/a
|
||||
import { SettingsConnectedAccountIcon } from '@/settings/accounts/components/SettingsConnectedAccountIcon';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledNameCell = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
background: ${themeCssVariables.background.transparent.light};
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
@@ -27,7 +29,7 @@ type SettingsConnectedAccountsTableRowProps = {
|
||||
export const SettingsConnectedAccountsTableRow = ({
|
||||
account,
|
||||
}: SettingsConnectedAccountsTableRowProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const IconComponent = SettingsConnectedAccountIcon({ account });
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconMinus, IconPlus } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null';
|
||||
|
||||
type SettingsCounterProps = {
|
||||
@@ -16,19 +17,21 @@ type SettingsCounterProps = {
|
||||
const StyledCounterContainer = styled.div<{ showButtons: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
margin-left: auto;
|
||||
width: ${({ theme, showButtons }) =>
|
||||
showButtons ? theme.spacing(30) : theme.spacing(16)};
|
||||
width: ${({ showButtons }) =>
|
||||
showButtons
|
||||
? themeCssVariables.spacing[30]
|
||||
: themeCssVariables.spacing[16]};
|
||||
`;
|
||||
|
||||
const StyledTextInput = styled(SettingsTextInput)`
|
||||
width: ${({ theme }) => theme.spacing(16)};
|
||||
width: ${themeCssVariables.spacing[16]};
|
||||
input {
|
||||
width: ${({ theme }) => theme.spacing(16)};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
width: ${themeCssVariables.spacing[16]};
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
text-align: center;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
+6
-5
@@ -1,11 +1,12 @@
|
||||
import { WebhookEntitySelect } from '@/settings/developers/components/WebhookEntitySelect';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconBox, IconNorthStar, IconPlus, IconTrash } from 'twenty-ui/display';
|
||||
import { IconButton, type SelectOption } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const OBJECT_DROPDOWN_WIDTH = 240;
|
||||
const ACTION_DROPDOWN_WIDTH = 240;
|
||||
@@ -18,14 +19,14 @@ const StyledFilterRow = styled.div<{ isMobile: boolean }>`
|
||||
isMobile
|
||||
? `${OBJECT_MOBILE_WIDTH}px ${ACTION_MOBILE_WIDTH}px auto`
|
||||
: `${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto`};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledPlaceholder = styled.div`
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
width: ${({ theme }) => theme.spacing(8)};
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
width: ${themeCssVariables.spacing[8]};
|
||||
`;
|
||||
|
||||
export const SettingsDatabaseEventsForm = ({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { OverflowingTextWithTooltip, Status } from 'twenty-ui/display';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledSettingsHeaderContainer = styled.div`
|
||||
align-items: center;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SettingsItemTypeTagProps = {
|
||||
item: {
|
||||
@@ -17,16 +19,16 @@ type SettingsItemTypeTagProps = {
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
`;
|
||||
|
||||
export const SettingsItemTypeTag = ({
|
||||
className,
|
||||
item: { isCustom, isRemote, applicationId },
|
||||
}: SettingsItemTypeTagProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const itemTagInfo = getItemTagInfo({
|
||||
item: { isCustom, isRemote, applicationId },
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { type ComponentType } from 'react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ComponentType, useContext } from 'react';
|
||||
|
||||
import { SettingsListSkeletonCard } from '@/settings/components/SettingsListSkeletonCard';
|
||||
|
||||
import { type IconComponent, IconPlus } from 'twenty-ui/display';
|
||||
import { Card, CardFooter } from 'twenty-ui/layout';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { SettingsListItemCardContent } from './SettingsListItemCardContent';
|
||||
|
||||
const StyledFooter = styled(CardFooter)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledButton = styled.button`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: 0 ${themeCssVariables.spacing[1]};
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
background: ${themeCssVariables.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -71,7 +72,7 @@ export const SettingsListCard = <
|
||||
to,
|
||||
rounded,
|
||||
}: SettingsListCardProps<ListItem>) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
if (isLoading === true) return <SettingsListSkeletonCard />;
|
||||
|
||||
|
||||
+24
-27
@@ -1,41 +1,33 @@
|
||||
import isPropValid from '@emotion/is-prop-valid';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronRight, type IconComponent } from 'twenty-ui/display';
|
||||
import { CardContent } from 'twenty-ui/layout';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledRow = styled(CardContent, {
|
||||
shouldForwardProp: (prop) => prop !== 'to' && isPropValid(prop),
|
||||
})<{ to?: boolean }>`
|
||||
const StyledRow = styled(CardContent)`
|
||||
align-items: center;
|
||||
cursor: ${({ onClick, to }) => (onClick || to ? 'pointer' : 'default')};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
min-height: ${({ theme }) => theme.spacing(6)};
|
||||
|
||||
&:hover {
|
||||
${({ to, theme }) =>
|
||||
to && `background: ${theme.background.transparent.light};`}
|
||||
}
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
padding-left: ${themeCssVariables.spacing[3]};
|
||||
min-height: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledRightContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
@@ -47,13 +39,13 @@ const StyledLabel = styled.span`
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
@@ -78,10 +70,15 @@ export const SettingsListItemCardContent = ({
|
||||
rightComponent,
|
||||
to,
|
||||
}: SettingsListItemCardContentProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const content = (
|
||||
<StyledRow onClick={onClick} divider={divider} to={!!to}>
|
||||
<StyledRow
|
||||
onClick={onClick}
|
||||
divider={divider}
|
||||
isClickable={!!onClick || !!to}
|
||||
hasHoverHighlight={!!to}
|
||||
>
|
||||
{!!LeftIcon && (
|
||||
<LeftIcon
|
||||
size={theme.icon.size.md}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Card } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
|
||||
+11
-10
@@ -1,4 +1,4 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { useMemo, useRef, useState, type MouseEvent } from 'react';
|
||||
|
||||
@@ -23,6 +23,7 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconBox, useIcons, type IconComponent } from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export type SelectSizeVariant = 'small' | 'default';
|
||||
|
||||
@@ -58,23 +59,23 @@ const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
display: block;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
font-size: ${themeCssVariables.font.size.xs};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
margin-bottom: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
`;
|
||||
|
||||
const StyledError = styled.span`
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
color: ${themeCssVariables.color.red};
|
||||
display: block;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
font-size: ${themeCssVariables.font.size.xs};
|
||||
margin-top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const SettingsMorphRelationMultiSelect = ({
|
||||
|
||||
+19
-18
@@ -1,4 +1,5 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type StyledCardContentProps = {
|
||||
disabled?: boolean;
|
||||
@@ -9,27 +10,27 @@ type StyledCardContentProps = {
|
||||
export const StyledSettingsCardContent = styled.div<StyledCardContentProps>`
|
||||
align-items: ${({ alignItems }) => alignItems ?? 'center'};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
height: ${({ fullHeight }) => (fullHeight ? '100%' : 'auto')};
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
padding: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
export const StyledSettingsCardIcon = styled.div`
|
||||
align-items: center;
|
||||
border: 2px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border: 2px solid ${themeCssVariables.border.color.light};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
background-color: ${themeCssVariables.background.primary};
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(7)};
|
||||
height: ${themeCssVariables.spacing[7]};
|
||||
justify-content: center;
|
||||
width: ${({ theme }) => theme.spacing(7)};
|
||||
min-width: ${({ theme }) => theme.icon.size.md};
|
||||
width: ${themeCssVariables.spacing[7]};
|
||||
min-width: ${themeCssVariables.icon.size.md};
|
||||
`;
|
||||
|
||||
export const StyledSettingsCardTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
margin-bottom: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const StyledSettingsCardTextContainer = styled.div`
|
||||
@@ -38,8 +39,8 @@ export const StyledSettingsCardTextContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const StyledSettingsCardDescription = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -51,7 +52,7 @@ export const StyledSettingsCardDescription = styled.div`
|
||||
`;
|
||||
|
||||
export const StyledSettingsCardThirdLine = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-size: ${themeCssVariables.font.size.xs};
|
||||
margin-top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
StyledSettingsCardTitle,
|
||||
} from '@/settings/components/SettingsOptions/SettingsCardContentBase';
|
||||
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
type IconComponent,
|
||||
OverflowingTextWithTooltip,
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
StyledSettingsCardTitle,
|
||||
} from '@/settings/components/SettingsOptions/SettingsCardContentBase';
|
||||
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
type IconComponent,
|
||||
OverflowingTextWithTooltip,
|
||||
|
||||
+8
-11
@@ -7,14 +7,15 @@ import {
|
||||
StyledSettingsCardTitle,
|
||||
} from '@/settings/components/SettingsOptions/SettingsCardContentBase';
|
||||
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useId } from 'react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext, useId } from 'react';
|
||||
import {
|
||||
type IconComponent,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { Toggle } from 'twenty-ui/input';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledSettingsCardToggleContent = styled(StyledSettingsCardContent)`
|
||||
cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
|
||||
@@ -22,15 +23,11 @@ const StyledSettingsCardToggleContent = styled(StyledSettingsCardContent)`
|
||||
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
background: ${themeCssVariables.background.transparent.lighter};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSettingsCardToggleButton = styled(Toggle)<{
|
||||
toggleCentered?: boolean;
|
||||
}>`
|
||||
align-self: ${({ toggleCentered }) =>
|
||||
toggleCentered ? 'center' : 'flex-start'};
|
||||
const StyledSettingsCardToggleButton = styled(Toggle)`
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
`;
|
||||
@@ -64,7 +61,7 @@ export const SettingsOptionCardContentToggle = ({
|
||||
checked,
|
||||
onChange,
|
||||
}: SettingsOptionCardContentToggleProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const toggleId = useId();
|
||||
|
||||
return (
|
||||
@@ -95,7 +92,7 @@ export const SettingsOptionCardContentToggle = ({
|
||||
disabled={disabled}
|
||||
toggleSize="small"
|
||||
color={advancedMode ? theme.color.yellow : theme.color.blue}
|
||||
toggleCentered={toggleCentered}
|
||||
centered={toggleCentered}
|
||||
/>
|
||||
</StyledSettingsCardToggleContent>
|
||||
{divider && <Separator />}
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
|
||||
type SettingsOptionIconCustomizerProps = {
|
||||
Icon: IconComponent;
|
||||
@@ -20,7 +21,7 @@ export const SettingsOptionIconCustomizer = ({
|
||||
zoom = 1,
|
||||
rotate = -4,
|
||||
}: SettingsOptionIconCustomizerProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return (
|
||||
<StyledIconCustomizer zoom={zoom} rotate={rotate}>
|
||||
<Icon
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import { IconUsers } from 'twenty-ui/display';
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type SelectValue } from '@/ui/input/components/internal/select/types';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import { IconBell, IconLock, IconRobot, IconUsers } from 'twenty-ui/display';
|
||||
|
||||
@@ -2,20 +2,22 @@ import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSet
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useScrollRestoration } from '@/ui/utilities/scroll/hooks/useScrollRestoration';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledSettingsPageContainer = styled.div<{
|
||||
width?: number;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
gap: ${themeCssVariables.spacing[8]};
|
||||
overflow: auto;
|
||||
padding: ${({ theme }) => theme.spacing(6, 8, 8)};
|
||||
padding: ${themeCssVariables.spacing[6]} ${themeCssVariables.spacing[8]}
|
||||
${themeCssVariables.spacing[8]};
|
||||
width: ${({ width }) => {
|
||||
if (isDefined(width)) {
|
||||
return width + 'px';
|
||||
@@ -25,7 +27,7 @@ const StyledSettingsPageContainer = styled.div<{
|
||||
}
|
||||
return OBJECT_SETTINGS_WIDTH + 'px';
|
||||
}};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(20)};
|
||||
padding-bottom: ${themeCssVariables.spacing[20]};
|
||||
`;
|
||||
|
||||
export const SettingsPageContainer = ({
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { CardContent } from 'twenty-ui/layout';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { Radio } from 'twenty-ui/input';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledRadioCardContent = styled(CardContent)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
flex-grow: 1;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
background: ${themeCssVariables.background.transparent.lighter};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRadio = styled(Radio)`
|
||||
margin-left: auto;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
`;
|
||||
|
||||
type SettingsRadioCardProps = {
|
||||
@@ -53,7 +55,7 @@ export const SettingsRadioCard = ({
|
||||
isSelected,
|
||||
Icon,
|
||||
}: SettingsRadioCardProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const onClick = () => handleSelect(value);
|
||||
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { SettingsRadioCard } from '@/settings/components/SettingsRadioCard';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledRadioCardContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
type SettingsRadioCardContainerProps = {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -12,11 +14,12 @@ const StyledContainer = styled.div`
|
||||
`;
|
||||
|
||||
const StyledTitleLoaderContainer = styled.div`
|
||||
margin: ${({ theme }) => theme.spacing(8, 8, 2)};
|
||||
margin: ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]}
|
||||
${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsSkeletonLoader = () => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return (
|
||||
<StyledContainer>
|
||||
<PageHeader
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { Card, CardContent } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SettingsSummaryCardProps = {
|
||||
title: ReactNode;
|
||||
@@ -10,16 +11,16 @@ type SettingsSummaryCardProps = {
|
||||
const StyledCardContent = styled(CardContent)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
min-height: ${({ theme }) => theme.spacing(6)};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
min-height: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-right: auto;
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user