animate user timezones

This commit is contained in:
Maxim 2022-09-06 16:23:06 +03:00
parent 1ce970aac2
commit 99877b99d2
15 changed files with 323 additions and 96 deletions

View file

@ -13,6 +13,7 @@ module.exports = {
'react/prop-types': 'warn',
'react/display-name': 'warn',
'react/jsx-key': 'warn',
'react-hooks/exhaustive-deps': 'off',
'react/no-unescaped-entities': 'warn',
'react/jsx-no-target-blank': 'warn',
'no-restricted-imports': 'warn',

View file

@ -4,19 +4,22 @@
.root table {
width: 100%;
background: #22252b;
}
.root tr {
border-bottom: 1px solid #33363b;
border-bottom: 1px solid #181b1f;
height: 60px;
}
.root tr:hover {
background: var(--secondary-background);
/* background: var(--secondary-background); */
background: rgba(63, 62, 62, 0.45);
}
.root td {
min-height: 60px;
padding: 10px 0;
}
.pagination {

View file

@ -43,16 +43,43 @@
height: 76px;
}
.user {
.avatar-group {
position: absolute;
top: 10px;
border: 2px solid #c65210;
transition: left 1s linear;
transform: translate(-50%, 0);
z-index: 0;
height: 32px;
}
.avatar {
position: absolute;
top: 0;
transition: opacity 200ms ease, left 200ms ease;
border-radius: 50%;
}
.is-oncall-icon {
color: var(--oncall-icon-stroke-color);
position: absolute;
left: -1px;
bottom: -1px;
}
.user-more {
position: absolute;
padding: 0 5px;
bottom: 0;
font-size: 12px;
line-height: 16px;
background: #454952;
border-radius: 8px;
text-align: center;
transition: opacity 200ms ease, left 200ms ease;
}
.avatar-group_inactive {
opacity: 0.2;
transition: opacity 0.5s ease;
}
.time-stripe {
position: relative;
height: 4px;
@ -91,10 +118,6 @@
width: 100%;
}
.time-mark {
}
.time-mark-text {
display: inline-block;
padding: 0 5px;

View file

@ -7,6 +7,7 @@ import dayjs from 'dayjs';
import Avatar from 'components/Avatar/Avatar';
import ScheduleUserDetails from 'components/ScheduleUserDetails/ScheduleUserDetails';
import Text from 'components/Text/Text';
import { IsOncallIcon } from 'icons';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
@ -16,6 +17,7 @@ interface UsersTimezonesProps {
users: User[];
tz: Timezone;
onTzChange: (tz: Timezone) => void;
onCallNow: Array<Partial<User>>;
}
const cx = cn.bind(styles);
@ -25,17 +27,11 @@ const hoursToSplit = 3;
const jLimit = 24 / hoursToSplit;
const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
const { users, tz, onTzChange } = props;
const { users, tz, onTzChange, onCallNow } = props;
const [count, setCount] = useState<number>(0);
const [currentMoment, setCurrentMoment] = useState<dayjs.Dayjs>(dayjs().tz(tz));
const getAvatarClickHandler = useCallback((user) => {
return () => {
onTzChange(user.timezone);
};
}, []);
useEffect(() => {
setCurrentMoment(currentMoment.tz(tz).startOf('minute'));
}, [tz]);
@ -73,7 +69,7 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<div className={cx('title')}>Team timezones</div>
<div className={cx('title')}>Schedule team and timezones</div>
{/* <HorizontalGroup>
<InlineSwitch transparent />
Current schedule users only
@ -88,32 +84,7 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
</div>
<div className={cx('users')}>
<div className={cx('current-time')} style={{ left: `${currentTimeX}%` }} />
{users.map((user, index) => {
const userCurrentMoment = dayjs(currentMoment).tz(user.timezone);
const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes');
const userHour = userCurrentMoment.hour();
const x = (diff / 1440) * 100;
return (
<Tooltip
interactive
key={index}
content={<ScheduleUserDetails currentMoment={currentMoment} user={user} />}
>
<div
className={cx('user')}
onClick={getAvatarClickHandler(user)}
style={{
left: `${x}%`,
opacity: userHour >= 9 && userHour < 18 ? 1 : 0.5,
}}
>
<Avatar src={user.avatar} size="large" />
</div>
</Tooltip>
);
})}
<UserAvatars users={users} onCallNow={onCallNow} onTzChange={onTzChange} currentMoment={currentMoment} />
</div>
<div className={cx('time-stripe')}>
<div className={cx('current-user-stripe')} />
@ -138,4 +109,170 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
);
};
interface UserAvatarsProps {
users: User[];
currentMoment: dayjs.Dayjs;
onTzChange: (timezone: Timezone) => void;
onCallNow: Array<Partial<User>>;
}
const UserAvatars = (props: UserAvatarsProps) => {
const { users, currentMoment, onTzChange, onCallNow } = props;
const userGroups = useMemo(() => {
return users
.reduce((memo, user) => {
let group = memo.find((group) => group.timezone === user.timezone);
if (!group) {
group = { timezone: user.timezone, users: [] };
memo.push(group);
}
group.users.push(user);
return memo;
}, [])
.sort((a, b) => {
const aOffset = dayjs().tz(a.timezone).utcOffset();
const bOffset = dayjs().tz(b.timezone).utcOffset();
if (aOffset > bOffset) {
return 1;
}
if (aOffset < bOffset) {
return -1;
}
return 0;
});
}, [users]);
const getAvatarClickHandler = useCallback((timezone: Timezone) => {
return () => {
onTzChange(timezone);
};
}, []);
const [activeTimezone, setActiveTimezone] = useState<Timezone | undefined>(undefined);
return (
<div className={cx('user-avatars')}>
{userGroups.map((group) => {
const userCurrentMoment = dayjs(currentMoment).tz(group.timezone);
const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes');
const xPos = (diff / (60 * 24)) * 100;
return (
<AvatarGroup
activeTimezone={activeTimezone}
timezone={group.timezone}
onSetActiveTimezone={setActiveTimezone}
onClick={getAvatarClickHandler(group.timezone)}
xPos={xPos}
users={group.users}
currentMoment={currentMoment}
onCallNow={onCallNow}
/>
);
})}
</div>
);
};
interface AvatarGroupProps {
users: User[];
xPos: number;
currentMoment: dayjs.Dayjs;
onClick: () => void;
timezone: Timezone;
onSetActiveTimezone: (timezone: Timezone) => void;
activeTimezone: Timezone;
onCallNow: Array<Partial<User>>;
}
const LIMIT = 3;
const AVATAR_WIDTH = 32;
const AVATAR_GAP = 5;
const AvatarGroup = (props: AvatarGroupProps) => {
const {
users: propsUsers,
currentMoment,
xPos,
onClick,
timezone,
onSetActiveTimezone,
activeTimezone,
onCallNow,
} = props;
const active = activeTimezone && activeTimezone === timezone;
const translateLeft = -AVATAR_WIDTH / 2;
const users = useMemo(() => {
return [...propsUsers].sort((a, b) => {
const aIsOncall = Number(onCallNow.some((onCallUser) => a.pk === onCallUser.pk));
const bIsOncall = Number(onCallNow.some((onCallUser) => b.pk === onCallUser.pk));
if (aIsOncall < bIsOncall) {
return 1;
}
if (aIsOncall > bIsOncall) {
return -1;
}
return 0;
});
}, [propsUsers]);
const width = active ? users.length * AVATAR_WIDTH + (users.length - 1) * AVATAR_GAP : AVATAR_WIDTH;
return (
<div
className={cx('avatar-group', { [`avatar-group_inactive`]: activeTimezone && activeTimezone !== timezone })}
style={{ width: `${width + AVATAR_GAP}px`, left: `${xPos}%`, transform: `translate(${translateLeft}px, 0)` }}
onClick={onClick}
onMouseEnter={() => onSetActiveTimezone(timezone)}
onMouseLeave={() => onSetActiveTimezone(undefined)}
>
{users.map((user, index, array) => {
const isOncall = onCallNow.some((onCallUser) => user.pk === onCallUser.pk);
return (
<Tooltip
placement="top"
interactive
key={index}
content={<ScheduleUserDetails currentMoment={currentMoment} user={user} />}
>
<div
className={cx('avatar')}
style={{
left: active ? `${index * (AVATAR_WIDTH + AVATAR_GAP)}px` : `${index * 10}px`,
opacity: active ? 1 : Math.max(1 - index * 0.25, 0.25),
visibility: !active && index >= LIMIT ? 'hidden' : 'visible',
zIndex: array.length - index - 1,
/* opacity: userHour >= 9 && userHour < 18 ? 1 : 0.5,*/
}}
>
<Avatar src={user.avatar} size="large" />
{isOncall && <IsOncallIcon className={cx('is-oncall-icon')} />}
</div>
</Tooltip>
);
})}
<div
style={{
opacity: !active && users.length > LIMIT ? '1' : '0',
zIndex: users.length,
left: active ? `${users.length * (AVATAR_WIDTH + AVATAR_GAP)}px` : `${users.length * 10}px`,
}}
className={cx('user-more')}
>
+{users.length - LIMIT}
</div>
</div>
);
};
export default UsersTimezones;

View file

@ -3,9 +3,8 @@ import React, { FC, useMemo, useState, useEffect, useRef, useCallback } from 're
import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { CSSTransitionGroup } from 'react-transition-group'; // ES6
import ScheduleSlot from 'components/ScheduleSlot/ScheduleSlot';
import ScheduleSlot from 'containers/ScheduleSlot/ScheduleSlot';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Rotation as RotationType, Schedule, Event } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
@ -20,6 +19,7 @@ const cx = cn.bind(styles);
interface ScheduleSlotState {}
interface RotationProps {
scheduleId: Schedule['id'];
startMoment: dayjs.Dayjs;
currentTimezone: Timezone;
layerIndex?: number;
@ -34,6 +34,7 @@ interface RotationProps {
const Rotation: FC<RotationProps> = (props) => {
const {
events,
scheduleId,
layerIndex,
rotationIndex,
startMoment,
@ -84,11 +85,6 @@ const Rotation: FC<RotationProps> = (props) => {
const dayOffset = Math.floor((x / width) * 7);
/* console.log('event.offsetX', event.offsetX);
console.log('event.nativeEvent', event.nativeEvent);
console.log('event.currentTarget', event.currentTarget);
console.log('dayOffset', dayOffset);
*/
onClick(startMoment.add(dayOffset, 'day'));
};
@ -125,6 +121,7 @@ const Rotation: FC<RotationProps> = (props) => {
return (
<ScheduleSlot
index={index}
scheduleId={scheduleId}
key={event.start}
event={event}
layerIndex={layerIndex}

View file

@ -114,6 +114,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
classNames={{ ...styles }}
>
<Rotation
scheduleId={scheduleId}
onClick={(moment) => {
this.onRotationClick(shiftId, moment);
}}
@ -147,6 +148,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
<TimelineMarks startMoment={startMoment} />
<div className={cx('rotations')}>
<Rotation
scheduleId={scheduleId}
onClick={(moment) => {
this.handleAddLayer(nextPriority, moment);
}}

View file

@ -5,6 +5,7 @@ import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
@ -14,6 +15,7 @@ import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
import { findColor } from './Rotations.helpers';
import styles from './Rotations.module.css';
@ -81,23 +83,33 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
<div className={cx('header-plus-content')}>
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
<TimelineMarks startMoment={startMoment} />
<div className={cx('rotations')}>
<TransitionGroup className={cx('rotations')}>
{shifts && shifts.length ? (
shifts.map(({ shiftId, events }, index) => {
return (
<Rotation
key={index}
events={events}
startMoment={startMoment}
currentTimezone={currentTimezone}
color={findColor(shiftId, layers, overrides)}
/>
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
<Rotation
key={index}
scheduleId={scheduleId}
events={events}
startMoment={startMoment}
currentTimezone={currentTimezone}
color={findColor(shiftId, layers, overrides)}
/>
</CSSTransition>
);
})
) : (
<Rotation events={[]} startMoment={startMoment} currentTimezone={currentTimezone} />
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
<Rotation
scheduleId={scheduleId}
events={[]}
startMoment={startMoment}
currentTimezone={currentTimezone}
/>
</CSSTransition>
)}
</div>
</TransitionGroup>
</div>
</div>
</>

View file

@ -80,6 +80,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
<CSSTransition key={rotationIndex} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
<Rotation
key={rotationIndex}
scheduleId={scheduleId}
events={events}
color={getOverrideColor(rotationIndex)}
startMoment={startMoment}
@ -92,9 +93,10 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
</CSSTransition>
))
) : (
<CSSTransition key={0} timeout={500} classNames={{ ...styles }}>
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
<Rotation
events={[]}
scheduleId={scheduleId}
startMoment={startMoment}
currentTimezone={currentTimezone}
onClick={(moment) => {

View file

@ -79,3 +79,8 @@
background-color: white;
z-index: 2;
}
.is-oncall-icon {
color: var(--oncall-icon-stroke-color);
margin-left: -2px;
}

View file

@ -8,7 +8,8 @@ import { observer } from 'mobx-react';
import Line from 'components/ScheduleUserDetails/img/line.svg';
import Text from 'components/Text/Text';
import WorkingHours from 'components/WorkingHours/WorkingHours';
import { Event } from 'models/schedule/schedule.types';
import { IsOncallIcon } from 'icons';
import { Event, Schedule } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { useStore } from 'state/useStore';
@ -18,10 +19,8 @@ import { getTitle } from './ScheduleSlot.helpers';
import styles from './ScheduleSlot.module.css';
interface ScheduleSlotProps {
index: number;
layerIndex: number;
rotationIndex: number;
event: Event;
scheduleId: Schedule['id'];
startMoment: dayjs.Dayjs;
currentTimezone: Timezone;
color?: string;
@ -31,7 +30,7 @@ interface ScheduleSlotProps {
const cx = cn.bind(styles);
const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color, label } = props;
const { event, scheduleId, startMoment, currentTimezone, color, label } = props;
const { users } = event;
const trackMouse = false;
@ -53,6 +52,8 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
setMouseX(event.nativeEvent.offsetX);
}, []);
const onCallNow = store.scheduleStore.items[scheduleId]?.on_call_now;
return (
<div className={cx('stack')} style={{ width: `${width * 100}%` /*left: `${x * 100}%`*/ }}>
{!event.is_gap ? (
@ -63,8 +64,21 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
const title = getTitle(storeUser);
const isOncall = Boolean(
storeUser && onCallNow && onCallNow.some((onCallUser) => storeUser.pk === onCallUser.pk)
);
return (
<Tooltip content={<ScheduleSlotDetails user={storeUser} currentTimezone={currentTimezone} event={event} />}>
<Tooltip
content={
<ScheduleSlotDetails
user={storeUser}
isOncall={isOncall}
currentTimezone={currentTimezone}
event={event}
/>
}
>
<div
className={cx('root', { root__inactive: inactive })}
style={{
@ -109,25 +123,20 @@ export default ScheduleSlot;
interface ScheduleSlotDetailsProps {
user: User;
isOncall: boolean;
currentTimezone: Timezone;
event: Event;
}
const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
const { user, currentTimezone, event } = props;
const userStatus = 'success';
const { user, currentTimezone, event, isOncall } = props;
return (
<div className={cx('details')}>
<HorizontalGroup>
<VerticalGroup spacing="sm">
<HorizontalGroup spacing="md">
<div
className={cx('details-user-status', {
[`details-user-status__type_${userStatus}`]: true,
})}
/>
<HorizontalGroup spacing="sm">
{isOncall && <IsOncallIcon className={cx('is-oncall-icon')} />}
<Text type="secondary">{user?.username}</Text>
</HorizontalGroup>
<HorizontalGroup>

View file

@ -238,3 +238,28 @@ export const ExpandIcon = (props: IconProps) => {
</svg>
);
};
interface IsOncallIconProps {
className: string;
}
export const IsOncallIcon = (props: IsOncallIconProps) => {
const { className } = props;
return (
<svg
className={className}
width="17"
height="16"
viewBox="0 0 17 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="8.72021" cy="8" r="7.5" fill="#6CCF8E" stroke="currentColor" />
<path
d="M6.91558 9.7903C7.32816 10.2082 7.76482 10.584 8.22558 10.9177C8.68634 11.2514 9.14709 11.5145 9.60785 11.7069C10.0686 11.9023 10.5038 12 10.9133 12C11.1934 12 11.4539 11.9504 11.6948 11.8512C11.9357 11.752 12.1541 11.5941 12.3498 11.3777C12.4612 11.2514 12.5501 11.1161 12.6163 10.9718C12.6856 10.8275 12.7202 10.6817 12.7202 10.5344C12.7202 10.4262 12.6976 10.3209 12.6525 10.2187C12.6073 10.1165 12.532 10.0293 12.4266 9.95716L11.1121 9.02818C11.0097 8.95303 10.9148 8.89891 10.8275 8.86584C10.7432 8.83277 10.6619 8.81623 10.5836 8.81623C10.4872 8.81623 10.3923 8.84329 10.299 8.89741C10.2086 8.94852 10.1153 9.02368 10.0189 9.12289L9.70723 9.42052C9.66507 9.46561 9.61086 9.48816 9.54461 9.48816C9.51148 9.48816 9.47986 9.48365 9.44975 9.47463C9.41963 9.46261 9.39403 9.45209 9.37295 9.44307C9.23744 9.37091 9.06428 9.24615 8.85347 9.06877C8.64568 8.89139 8.43638 8.69748 8.22558 8.48703C8.01176 8.27659 7.81602 8.06614 7.63834 7.85569C7.46066 7.64525 7.33719 7.47388 7.26793 7.3416C7.25588 7.31755 7.24384 7.292 7.23179 7.26494C7.22276 7.23487 7.21824 7.2018 7.21824 7.16573C7.21824 7.10259 7.24082 7.04998 7.286 7.00789L7.58865 6.70124C7.68502 6.60203 7.76031 6.50733 7.81451 6.41714C7.86872 6.32394 7.89582 6.22773 7.89582 6.12852C7.89582 6.05036 7.87775 5.96918 7.84162 5.88501C7.80849 5.79782 7.75579 5.70312 7.68351 5.6009L6.75748 4.30665C6.68521 4.20143 6.59637 4.12477 6.49097 4.07666C6.38556 4.02555 6.27263 4 6.15217 4C5.85705 4 5.5815 4.12326 5.32552 4.36979C5.11472 4.57121 4.96113 4.79369 4.86477 5.0372C4.7684 5.28072 4.72021 5.53927 4.72021 5.81285C4.72021 6.22473 4.81508 6.65915 5.0048 7.11612C5.19753 7.57309 5.45953 8.03006 5.7908 8.48703C6.12206 8.941 6.49699 9.37542 6.91558 9.7903Z"
fill="currentColor"
/>
</svg>
);
};

View file

@ -135,7 +135,12 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
Users from on-call schedule" step in escalation chains.
</Text>
<div className={cx('users-timezones')}>
<UsersTimezones users={users || []} tz={currentTimezone} onTzChange={this.handleTimezoneChange} />
<UsersTimezones
onCallNow={schedule?.on_call_now || []}
users={users || []}
tz={currentTimezone}
onTzChange={this.handleTimezoneChange}
/>
</div>
<div className={cx('controls')}>
<HorizontalGroup justify="space-between">

View file

@ -70,14 +70,14 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
const schedules = scheduleStore.getSearchResult();
const columns = [
/* {
{
width: '10%',
title: 'Status',
key: 'name',
render: this.renderStatus,
},*/
},
{
width: '50%',
width: '30%',
title: 'Name',
key: 'name',
render: this.renderName,
@ -88,18 +88,18 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
key: 'users',
render: this.renderOncallNow,
},
/*{
/* {
width: '20%',
title: 'ChatOps',
key: 'chatops',
render: this.renderChatOps,
},
},*/
{
width: '10%',
title: 'Quality',
key: 'quality',
render: this.renderQuality,
},*/
},
{
width: '5%',
key: 'buttons',
@ -254,16 +254,20 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
renderOncallNow = (item: Schedule, index: number) => {
if (item.on_call_now?.length > 0) {
return item.on_call_now.map((user, index) => {
return (
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }}>
<div>
<Avatar size="small" src={user.avatar} />
<Text type="secondary"> {user.username}</Text>
</div>
</PluginLink>
);
});
return (
<VerticalGroup>
{item.on_call_now.map((user, index) => {
return (
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }}>
<div>
<Avatar size="big" src={user.avatar} />
<Text type="secondary"> {user.username}</Text>
</div>
</PluginLink>
);
})}
</VerticalGroup>
);
}
return null;
};
@ -275,7 +279,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
renderQuality = (item: Schedule) => {
const type = item.quality > 70 ? 'primary' : 'warning';
return <Text type={type}>{item.quality}%</Text>;
return <Text type={type}>{item.quality || 70}%</Text>;
};
renderButtons = (item: Schedule) => {

View file

@ -27,6 +27,7 @@
--primary-text-link: #1f62e0;
--timeline-icon-background: rgba(70, 76, 84, 0);
--timeline-icon-background-resolution-note: rgba(50, 116, 217, 0);
--oncall-icon-stroke-color: #fff;
}
.theme-dark {
@ -50,4 +51,5 @@
--hover-selected: rgba(204, 204, 220, 0.12);
--hover-selected-hardcoded: #34363d;
--secondary-background-shade: rgba(204, 204, 220, 0.2);
--oncall-icon-stroke-color: #181b1f;
}