mirror of
https://github.com/finos/SymphonyElectron.git
synced 2026-10-25 06:17:38 -05:00
Merge pull request #2302 from NguyenTranHoangSym/SDA-4732
SDA-4732: Revamp the call notification
This commit is contained in:
8 files changed
+320
-85
No files matched your search
@@ -154,4 +154,68 @@ describe('Call toast notification component', () => {
|
||||
externalBadge = wrapper.find('.ext-badge-container');
|
||||
expect(externalBadge.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display check for new federation toast - Unknown User', () => {
|
||||
const isExternal = true;
|
||||
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
isExternal,
|
||||
isFederatedEnabled: true,
|
||||
callerName: '',
|
||||
callerNumber: '123',
|
||||
primaryText: '123 [PHONE]',
|
||||
});
|
||||
|
||||
let federationTypeText = wrapper.find('[data-testid="FEDERATION_TYPE"]');
|
||||
let federationUnknownUserAvatar = wrapper.find(
|
||||
'[data-testid="FEDERATION_UNKNOWN_USER_AVATAR"]',
|
||||
);
|
||||
let federationPrimaryText = wrapper.find(
|
||||
'[data-testid="CALL_NOTIFICATION_NAME"]',
|
||||
);
|
||||
let avatarBadge = wrapper.find('[data-testid="AVATAR_BADGE"]');
|
||||
|
||||
expect(federationTypeText.exists()).toBeTruthy();
|
||||
expect(federationUnknownUserAvatar.exists()).toBeTruthy();
|
||||
expect(federationPrimaryText.exists()).toBeTruthy();
|
||||
expect(avatarBadge.exists()).toBeFalsy();
|
||||
|
||||
expect(federationPrimaryText.text()).toBe('123 ');
|
||||
expect(federationTypeText.text()).toBe('SMS & Voice');
|
||||
});
|
||||
|
||||
it('should display check for new federation toast - Known User', () => {
|
||||
const isExternal = true;
|
||||
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
isExternal,
|
||||
isFederatedEnabled: true,
|
||||
callerName: 'Helen',
|
||||
callerNumber: '123',
|
||||
primaryText: 'Helen',
|
||||
});
|
||||
let federationTypeText = wrapper.find('[data-testid="FEDERATION_TYPE"]');
|
||||
let federationUnknownUserAvatar = wrapper.find(
|
||||
'[data-testid="FEDERATION_NAMED_USER_AVATAR"]',
|
||||
);
|
||||
let federationNumber = wrapper.find(
|
||||
'[data-testid="FEDERATION_NAMED_USER_NUMBER"]',
|
||||
);
|
||||
let federationPrimaryText = wrapper.find(
|
||||
'[data-testid="CALL_NOTIFICATION_NAME"]',
|
||||
);
|
||||
let avatarBadge = wrapper.find('[data-testid="AVATAR_BADGE"]');
|
||||
|
||||
expect(federationTypeText.exists()).toBeTruthy();
|
||||
expect(federationUnknownUserAvatar.exists()).toBeTruthy();
|
||||
expect(federationPrimaryText.exists()).toBeTruthy();
|
||||
expect(avatarBadge.exists()).toBeFalsy();
|
||||
expect(federationNumber.exists()).toBeTruthy();
|
||||
|
||||
expect(federationPrimaryText.text()).toBe('Helen');
|
||||
expect(federationTypeText.text()).toBe('SMS & Voice');
|
||||
expect(federationNumber.text()).toBe('123');
|
||||
});
|
||||
});
|
||||
@@ -281,10 +281,14 @@ export enum KeyCodes {
|
||||
type Theme = '' | 'light' | 'dark';
|
||||
type CallType = 'IM' | 'ROOM' | 'OTHER';
|
||||
|
||||
export const CallType = { IM: 'IM', ROOM: 'ROOM', OTHER: 'OTHER' };
|
||||
|
||||
/**
|
||||
* Notification
|
||||
*/
|
||||
export interface INotificationData {
|
||||
isPhone?: boolean;
|
||||
notificationType?: string;
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
@@ -330,7 +334,12 @@ export interface ICallNotificationData {
|
||||
shouldDisplayBadge: boolean;
|
||||
acceptButtonText: string;
|
||||
rejectButtonText: string;
|
||||
isFederatedEnabled?: boolean;
|
||||
isPhone?: boolean;
|
||||
notificationType?: string;
|
||||
zoomFactor: number;
|
||||
callerNumber: string;
|
||||
callerName?: string;
|
||||
}
|
||||
|
||||
export enum NotificationActions {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30 10C30 4.47715 25.5228 0 20 0C14.4772 0 10 4.47715 10 10C10 15.5228 14.4772 20 20 20C25.5228 20 30 15.5228 30 10ZM25 10C25 12.7614 22.7614 15 20 15C17.2386 15 15 12.7614 15 10C15 7.23858 17.2386 5 20 5C22.7614 5 25 7.23858 25 10Z" fill="#378535"/>
|
||||
<path d="M2.5 30.5902C2.5 30.2021 2.59098 29.8179 2.84302 29.5228C3.91036 28.2729 7.864 25 20 25C32.136 25 36.0896 28.2729 37.157 29.5228C37.409 29.8179 37.5 30.2021 37.5 30.5902V37.5C37.5 38.8807 36.3807 40 35 40H5C3.61929 40 2.5 38.8807 2.5 37.5V30.5902ZM20 30C12.4729 30 8.94804 31.3462 7.5 32.167V35H32.5V32.167C31.052 31.3462 27.5271 30 20 30Z" fill="#378535"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.499 0A1.5 1.5 0 0114 1.501v12.998c0 .829-.672 1.501-1.501 1.501H3.501A1.501 1.501 0 012 14.499V1.501A1.5 1.5 0 013.501 0h8.998zM5.196 2H4v12h8V2h-1.196a.498.498 0 00-.48.364l-.117.409c-.124.43-.514.728-.961.728H6.754c-.447 0-.837-.298-.961-.728l-.117-.409A.498.498 0 005.196 2z" fill="#CDCFD4"/></svg>
|
||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.499 0A1.5 1.5 0 0114 1.501v12.998c0 .829-.672 1.501-1.501 1.501H3.501A1.501 1.501 0 012 14.499V1.501A1.5 1.5 0 013.501 0h8.998zM5.196 2H4v12h8V2h-1.196a.498.498 0 00-.48.364l-.117.409c-.124.43-.514.728-.961.728H6.754c-.447 0-.837-.298-.961-.728l-.117-.409A.498.498 0 005.196 2z" fill="#52565E"/></svg>
|
||||
|
After Width: | Height: | Size: 389 B |
@@ -1,6 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
import { CallType } from '../../common/api-interface';
|
||||
import { isMac } from '../../common/env';
|
||||
import {
|
||||
darkTheme,
|
||||
@@ -37,6 +38,10 @@ interface ICallNotificationState {
|
||||
isSecondaryTextOverflowing: boolean;
|
||||
isFederatedEnabled: boolean;
|
||||
zoomFactor: number;
|
||||
isPhone?: boolean;
|
||||
notificationType?: string;
|
||||
callerNumber?: string;
|
||||
callerName?: string;
|
||||
}
|
||||
|
||||
type mouseEventButton =
|
||||
@@ -80,6 +85,7 @@ export default class CallNotification extends React.Component<
|
||||
isSecondaryTextOverflowing: false,
|
||||
isFederatedEnabled: false,
|
||||
zoomFactor: 1,
|
||||
callerNumber: '',
|
||||
};
|
||||
this.state = { ...this.defaultState };
|
||||
this.updateState = this.updateState.bind(this);
|
||||
@@ -126,6 +132,9 @@ export default class CallNotification extends React.Component<
|
||||
isSecondaryTextOverflowing,
|
||||
isFederatedEnabled,
|
||||
zoomFactor,
|
||||
isPhone,
|
||||
callerNumber,
|
||||
callerName,
|
||||
} = this.state;
|
||||
|
||||
let themeClassName;
|
||||
@@ -163,6 +172,133 @@ export default class CallNotification extends React.Component<
|
||||
: 'answer';
|
||||
const rejectText = rejectButtonText ? rejectButtonText : 'decline';
|
||||
|
||||
const renderAvatarSection = () => {
|
||||
return (
|
||||
<div className='logo-container'>
|
||||
{this.renderImage(
|
||||
icon,
|
||||
profilePlaceHolderText,
|
||||
callType,
|
||||
shouldDisplayBadge,
|
||||
isExternal,
|
||||
isFederatedEnabled,
|
||||
callerName ?? '',
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderNameSection = () => {
|
||||
return (
|
||||
<div className='info-text-container'>
|
||||
<div className='primary-text-container'>
|
||||
<div className='caller-name-container'>
|
||||
<div
|
||||
data-testid='CALL_NOTIFICATION_NAME'
|
||||
className={`caller-name ${themeClassName} tooltip-trigger`}
|
||||
ref={this.primaryTooltipRef}
|
||||
>
|
||||
{primaryText}
|
||||
</div>
|
||||
{isPrimaryTextOverflowing && (
|
||||
<div className='tooltip-content tooltip-primary'>
|
||||
{primaryText}
|
||||
</div>
|
||||
)}
|
||||
{this.renderExtBadge(isExternal)}
|
||||
</div>
|
||||
</div>
|
||||
{isFederatedEnabled ? (
|
||||
<>
|
||||
{callerName && (
|
||||
<div
|
||||
className='secondary-text-container'
|
||||
data-testid='FEDERATION_NAMED_USER_NUMBER'
|
||||
>
|
||||
<div className='caller-details'>
|
||||
<div
|
||||
className={`caller-role ${themeClassName} tooltip-trigger`}
|
||||
ref={this.secondaryTooltipRef}
|
||||
>
|
||||
{callerNumber}
|
||||
</div>
|
||||
{isSecondaryTextOverflowing && (
|
||||
<div className='tooltip-content tooltip-secondary'>
|
||||
{callerNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className='tertiary-text-container'
|
||||
data-testid='FEDERATION_TYPE'
|
||||
>
|
||||
<div className='application-details'>
|
||||
{isPhone && (
|
||||
<img
|
||||
className={'company-icon'}
|
||||
src={
|
||||
theme === Themes.LIGHT
|
||||
? '../renderer/assets/phone-light.svg'
|
||||
: '../renderer/assets/phone-dark.svg'
|
||||
}
|
||||
alt={'Federation'}
|
||||
/>
|
||||
)}
|
||||
<div className={`company-name ${themeClassName}`}>
|
||||
SMS & Voice
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{secondaryText ? (
|
||||
<div className='secondary-text-container'>
|
||||
<div className='caller-details'>
|
||||
<div
|
||||
className={`caller-role ${themeClassName} tooltip-trigger`}
|
||||
ref={this.secondaryTooltipRef}
|
||||
>
|
||||
{secondaryText}
|
||||
</div>
|
||||
{isSecondaryTextOverflowing && (
|
||||
<div className='tooltip-content tooltip-secondary'>
|
||||
{secondaryText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{company || companyIconUrl ? (
|
||||
<div className='tertiary-text-container'>
|
||||
<div className='application-details'>
|
||||
{company && companyIconUrl && (
|
||||
<img
|
||||
className={'company-icon'}
|
||||
src={companyIconUrl}
|
||||
alt={'company logo'}
|
||||
/>
|
||||
)}
|
||||
{callType !== CallType.ROOM && (
|
||||
<div className={`company-name ${themeClassName}`}>
|
||||
{company}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid='CALL_NOTIFICATION_CONTAINER'
|
||||
@@ -177,76 +313,13 @@ export default class CallNotification extends React.Component<
|
||||
{title}
|
||||
</div>
|
||||
<div className='caller-info-container' style={{ zoom: zoomFactor }}>
|
||||
<div className='logo-container'>
|
||||
{this.renderImage(
|
||||
icon,
|
||||
profilePlaceHolderText,
|
||||
callType,
|
||||
shouldDisplayBadge,
|
||||
isExternal,
|
||||
)}
|
||||
</div>
|
||||
<div className='info-text-container'>
|
||||
<div className='primary-text-container'>
|
||||
<div className='caller-name-container'>
|
||||
<div
|
||||
data-testid='CALL_NOTIFICATION_NAME'
|
||||
className={`caller-name ${themeClassName} tooltip-trigger`}
|
||||
ref={this.primaryTooltipRef}
|
||||
>
|
||||
{primaryText}
|
||||
</div>
|
||||
{isPrimaryTextOverflowing && (
|
||||
<div className='tooltip-content tooltip-primary'>
|
||||
{primaryText}
|
||||
</div>
|
||||
)}
|
||||
{this.renderExtBadge(isExternal)}
|
||||
</div>
|
||||
</div>
|
||||
{secondaryText ? (
|
||||
<div className='secondary-text-container'>
|
||||
<div className='caller-details'>
|
||||
<div
|
||||
className={`caller-role ${themeClassName} tooltip-trigger`}
|
||||
ref={this.secondaryTooltipRef}
|
||||
>
|
||||
{secondaryText}
|
||||
</div>
|
||||
{isSecondaryTextOverflowing && (
|
||||
<div className='tooltip-content tooltip-secondary'>
|
||||
{secondaryText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{company || companyIconUrl ? (
|
||||
<div className='tertiary-text-container'>
|
||||
<div className='application-details'>
|
||||
{company && companyIconUrl && (
|
||||
<img
|
||||
className={'company-icon'}
|
||||
src={companyIconUrl}
|
||||
alt={'company logo'}
|
||||
/>
|
||||
)}
|
||||
<div className={`company-name ${themeClassName}`}>
|
||||
{company}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
{renderAvatarSection()}
|
||||
{renderNameSection()}
|
||||
</div>
|
||||
<div className='actions' style={{ zoom: zoomFactor }}>
|
||||
<button
|
||||
data-testid='CALL_NOTIFICATION_REJECT_BUTTON'
|
||||
className={classNames('decline', {
|
||||
className={classNames('decline', 'call-button', {
|
||||
'call-type-other': callType === 'OTHER',
|
||||
})}
|
||||
onClick={this.eventHandlers.onReject(id)}
|
||||
@@ -255,7 +328,7 @@ export default class CallNotification extends React.Component<
|
||||
</button>
|
||||
<button
|
||||
data-testid='CALL_NOTIFICATION_ACCEPT_BUTTON'
|
||||
className={classNames('accept', {
|
||||
className={classNames('accept', 'call-button', {
|
||||
'call-type-other': callType === 'OTHER',
|
||||
})}
|
||||
onClick={this.eventHandlers.onAccept(id)}
|
||||
@@ -326,6 +399,8 @@ export default class CallNotification extends React.Component<
|
||||
: data.theme
|
||||
? data.theme
|
||||
: Themes.LIGHT;
|
||||
data.company = data.company === 'undefined' ? '' : data.company;
|
||||
data.primaryText = data.primaryText?.replace('[PHONE]', '') ?? 'unknown';
|
||||
this.setState(data as ICallNotificationState);
|
||||
this.checkTextOverflow();
|
||||
}
|
||||
@@ -350,6 +425,8 @@ export default class CallNotification extends React.Component<
|
||||
callType: CallType,
|
||||
shouldDisplayBadge: boolean,
|
||||
isExternal: boolean,
|
||||
isFederatedEnabled: boolean,
|
||||
callerName: string,
|
||||
): JSX.Element | undefined {
|
||||
let imgClass = 'default-logo';
|
||||
let url = '../renderer/assets/notification-symphony-logo.svg';
|
||||
@@ -360,29 +437,54 @@ export default class CallNotification extends React.Component<
|
||||
url = imageUrl;
|
||||
alt = 'Profile picture';
|
||||
}
|
||||
if (!imageUrl) {
|
||||
const profilePlaceHolderClassName =
|
||||
callType === 'IM'
|
||||
? 'profilePlaceHolderContainer'
|
||||
: 'roomPlaceHolderContainer';
|
||||
|
||||
const profilePlaceHolderClassName =
|
||||
callType === 'IM'
|
||||
? 'profilePlaceHolderContainer'
|
||||
: 'roomPlaceHolderContainer';
|
||||
|
||||
if (!callerName && isFederatedEnabled) {
|
||||
return (
|
||||
<div
|
||||
className={classNames('thumbnail', profilePlaceHolderClassName, {
|
||||
external: isExternal && !isFederatedEnabled,
|
||||
federation: isFederatedEnabled,
|
||||
})}
|
||||
>
|
||||
<img
|
||||
data-testid='FEDERATION_UNKNOWN_USER_AVATAR'
|
||||
className={classNames('default-logo')}
|
||||
src='../renderer/assets/federation-user.svg'
|
||||
alt={alt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageUrl || isFederatedEnabled) {
|
||||
return (
|
||||
<div className='logo'>
|
||||
<div
|
||||
data-testid={
|
||||
isFederatedEnabled ? 'FEDERATION_NAMED_USER_AVATAR' : 'AVATAR'
|
||||
}
|
||||
className={classNames('thumbnail', profilePlaceHolderClassName, {
|
||||
external: isExternal,
|
||||
external: isExternal && !isFederatedEnabled,
|
||||
federation: isFederatedEnabled,
|
||||
})}
|
||||
>
|
||||
<p className={'profilePlaceHolderText'}>{profilePlaceHolderText}</p>
|
||||
</div>
|
||||
{this.renderSymphonyBadge(shouldDisplayBadge, callType)}
|
||||
{!isFederatedEnabled &&
|
||||
this.renderSymphonyBadge(shouldDisplayBadge, callType)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='logo'>
|
||||
<div className='logo' data-testid={'AVATAR'}>
|
||||
<img className={imgClass} src={url} alt={alt} />
|
||||
{this.renderSymphonyBadge(shouldDisplayBadge)}
|
||||
{!isFederatedEnabled && this.renderSymphonyBadge(shouldDisplayBadge)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -401,6 +503,7 @@ export default class CallNotification extends React.Component<
|
||||
if (hasImageUrl) {
|
||||
return (
|
||||
<img
|
||||
data-testid='AVATAR_BADGE'
|
||||
src='../renderer/assets/symphony-badge.svg'
|
||||
alt=''
|
||||
className={`profile-picture-badge ${badgePositionClass}`}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
--profile-place-holder-text: @electricity-ui-50;
|
||||
--profile-place-holder-text-ext: @yellow-50;
|
||||
--profile-placeholder-ext-border-color: @yellow-20;
|
||||
--profile-placeholder-ext-border-color: @yellow-20;
|
||||
--profile-placeholder-fed-border-color: @green-30;
|
||||
--profile-place-holder-text-fed: @green-50;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -31,7 +34,8 @@
|
||||
--notification-border-color: @graphite-60;
|
||||
--profile-place-holder-text: @electricity-ui-40;
|
||||
--profile-place-holder-text-ext: @yellow-30;
|
||||
--profile-placeholder-ext-border-color: @yellow-30;
|
||||
--profile-placeholder-fed-border-color: @green-30;
|
||||
--profile-place-holder-text-fed: @green-30;
|
||||
}
|
||||
|
||||
.mac {
|
||||
@@ -109,6 +113,19 @@ body {
|
||||
.logo-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
.federation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-color: var(--profile-placeholder-fed-border-color);
|
||||
.profilePlaceHolderText {
|
||||
color: var(--profile-place-holder-text-fed);
|
||||
}
|
||||
}
|
||||
.default-logo {
|
||||
top: 0;
|
||||
width: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -117,11 +134,6 @@ body {
|
||||
width: 86px;
|
||||
border-radius: 70px;
|
||||
}
|
||||
.default-logo {
|
||||
top: 0;
|
||||
width: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.profile-picture-badge {
|
||||
width: 32px;
|
||||
position: absolute;
|
||||
@@ -245,13 +257,13 @@ text {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-basis: calc(100% / 2);
|
||||
padding: 4px 8px;
|
||||
padding: 4px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -286,7 +298,19 @@ text {
|
||||
background-color: @red-50;
|
||||
|
||||
&:hover {
|
||||
background-color: @red-40;
|
||||
background-color: @red-60;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: @red-70;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid @blue-40;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: @red-60;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,8 +322,31 @@ text {
|
||||
background-color: @green-50;
|
||||
|
||||
&:hover {
|
||||
background-color: @green-40;
|
||||
background-color: @green-60;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: @green-70;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid @blue-40;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: @green-60;
|
||||
}
|
||||
}
|
||||
|
||||
.call-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px 16px !important;
|
||||
|
||||
max-width: 79px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
|
||||
@@ -27,10 +27,16 @@
|
||||
@vanilla-white: #fff;
|
||||
@black: #000000;
|
||||
@blue: #008eff;
|
||||
@blue-40: #2996fd;
|
||||
|
||||
@red-05: #fbeeed;
|
||||
@red-70: #662a24;
|
||||
@red-60: #99342c;
|
||||
@red-50: #dd342e;
|
||||
@red-40: #ff5d50;
|
||||
@green-30: #65c862;
|
||||
@green-70: #294425;
|
||||
@green-60: #32602f;
|
||||
@green-50: #378535;
|
||||
@green-40: #2eaa35;
|
||||
|
||||
|
||||
Reference in new issue
Block a user