} onClick={openModal}>
@@ -112,9 +120,9 @@ export const TeamCard: FC = ({ team, isDeleting, onOpenDelete, o
-
+
Team settings
-
+
>
)}
diff --git a/src/Components/ThemeSwitch/Components/Crescent.tsx b/src/Components/ThemeSwitch/Components/Crescent.tsx
new file mode 100644
index 000000000..e0c776adc
--- /dev/null
+++ b/src/Components/ThemeSwitch/Components/Crescent.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+
+export const Crescent = ({ color, size }: { color: string; size: number }) => {
+ return (
+
+ );
+};
diff --git a/src/Components/ThemeSwitch/Components/Desktop.tsx b/src/Components/ThemeSwitch/Components/Desktop.tsx
new file mode 100644
index 000000000..404eecd6a
--- /dev/null
+++ b/src/Components/ThemeSwitch/Components/Desktop.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+
+export const Desktop = ({ color, size }: { color: string; size: number }) => {
+ return (
+
+ );
+};
diff --git a/src/Components/ThemeSwitch/Components/Sun.tsx b/src/Components/ThemeSwitch/Components/Sun.tsx
new file mode 100644
index 000000000..3e6f82e04
--- /dev/null
+++ b/src/Components/ThemeSwitch/Components/Sun.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+
+export const Sun = ({ color, size }: { color: string; size: number }) => {
+ return (
+
+ );
+};
diff --git a/src/Components/ThemeSwitch/Components/ThemeSwitchIcon/ThemeSwitchIcon.less b/src/Components/ThemeSwitch/Components/ThemeSwitchIcon/ThemeSwitchIcon.less
new file mode 100644
index 000000000..987b54efa
--- /dev/null
+++ b/src/Components/ThemeSwitch/Components/ThemeSwitchIcon/ThemeSwitchIcon.less
@@ -0,0 +1,14 @@
+@import "~styles/variables.less";
+
+.icon {
+ display: inline-flex;
+ vertical-align: -4px;
+ color: var(--text-primary);
+}
+
+.icon-container {
+ display: inline-flex;
+ vertical-align: -4px;
+ cursor: pointer;
+ gap: 4px;
+}
diff --git a/src/Components/ThemeSwitch/Components/ThemeSwitchIcon/ThemeSwitchIcon.tsx b/src/Components/ThemeSwitch/Components/ThemeSwitchIcon/ThemeSwitchIcon.tsx
new file mode 100644
index 000000000..81ee5923e
--- /dev/null
+++ b/src/Components/ThemeSwitch/Components/ThemeSwitchIcon/ThemeSwitchIcon.tsx
@@ -0,0 +1,63 @@
+import React, { FC } from "react";
+import { EThemesNames } from "../../../../Themes/themesNames";
+import { useTheme } from "../../../../Themes";
+import { Sun } from "../Sun";
+import { Crescent } from "../Crescent";
+import { Desktop } from "../Desktop";
+import classNames from "classnames/bind";
+
+import styles from "./ThemeSwitchIcon.less";
+
+const cn = classNames.bind(styles);
+
+interface IThemeSwitchIconProps {
+ currentTheme: EThemesNames;
+ onToggleThemeModal: () => void;
+}
+
+export const ThemeSwitchIcon: FC
= ({
+ currentTheme,
+ onToggleThemeModal,
+}) => {
+ const theme = useTheme();
+
+ const Icon = (() => {
+ switch (currentTheme) {
+ case EThemesNames.Light:
+ return (
+ <>
+
+
+
+ Light Theme
+ >
+ );
+ case EThemesNames.Dark:
+ return (
+ <>
+
+
+
+ Dark Theme
+ >
+ );
+ case EThemesNames.System:
+ return (
+ <>
+
+
+
+ System Theme
+ >
+ );
+ default:
+ return null;
+ }
+ })();
+
+ return (
+
+ {Icon}
+
+ );
+};
diff --git a/src/Components/ThemeSwitch/Components/ThemeSwitchRadio/ThemeSwitchRadio.less b/src/Components/ThemeSwitch/Components/ThemeSwitchRadio/ThemeSwitchRadio.less
new file mode 100644
index 000000000..2b9b8e0ac
--- /dev/null
+++ b/src/Components/ThemeSwitch/Components/ThemeSwitchRadio/ThemeSwitchRadio.less
@@ -0,0 +1,13 @@
+.theme-icon {
+ width: 60px;
+ margin: 0 20px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
+
+.theme-icon-text {
+ margin: 0;
+}
diff --git a/src/Components/ThemeSwitch/Components/ThemeSwitchRadio/ThemeSwitchRadio.tsx b/src/Components/ThemeSwitch/Components/ThemeSwitchRadio/ThemeSwitchRadio.tsx
new file mode 100644
index 000000000..4c6ddaf99
--- /dev/null
+++ b/src/Components/ThemeSwitch/Components/ThemeSwitchRadio/ThemeSwitchRadio.tsx
@@ -0,0 +1,49 @@
+import React, { CSSProperties, ReactElement, useState } from "react";
+import { useTheme } from "../../../../Themes";
+import { EThemesNames } from "../../../../Themes/themesNames";
+import classNames from "classnames/bind";
+
+import styles from "./ThemeSwitchRadio.less";
+
+const cn = classNames.bind(styles);
+
+interface ThemeSwitchRadioProps {
+ onThemeChange: (value: EThemesNames) => void;
+ themeName: EThemesNames;
+ text: string;
+ renderIcon: (color: string) => ReactElement;
+ currentThemeName: string;
+}
+
+export const ThemeSwitchRadio = (props: ThemeSwitchRadioProps) => {
+ const { onThemeChange, themeName, text, renderIcon, currentThemeName } = props;
+
+ const theme = useTheme();
+ const [hover, setHover] = useState(false);
+
+ let themeIconTextStyle: CSSProperties = { color: theme.iconColor };
+ let iconColor = theme.iconColor;
+
+ if (currentThemeName === themeName) {
+ themeIconTextStyle = { color: theme.iconCheckedColor };
+ iconColor = theme.iconCheckedColor;
+ } else if (hover) {
+ themeIconTextStyle = { color: theme.iconHoverColor };
+ iconColor = theme.iconHoverColor;
+ }
+
+ return (
+ setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ onClick={() => onThemeChange(themeName)}
+ >
+
+
+ );
+};
diff --git a/src/Components/ThemeSwitch/ThemeSwitch.less b/src/Components/ThemeSwitch/ThemeSwitch.less
new file mode 100644
index 000000000..954e9c501
--- /dev/null
+++ b/src/Components/ThemeSwitch/ThemeSwitch.less
@@ -0,0 +1,26 @@
+.themeSwitch {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.iconsContainer {
+ display: flex;
+ flex-direction: row;
+ position: relative;
+}
+
+.firstLine,
+.secondLine {
+ content: " ";
+ position: absolute;
+ left: 74px;
+ background-color: #858585;
+ width: 54px;
+ height: 1px;
+}
+
+.secondLine {
+ left: 176px;
+}
diff --git a/src/Components/ThemeSwitch/ThemeSwitch.tsx b/src/Components/ThemeSwitch/ThemeSwitch.tsx
new file mode 100644
index 000000000..93a6a9d6b
--- /dev/null
+++ b/src/Components/ThemeSwitch/ThemeSwitch.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import { Sun } from "./Components/Sun";
+import { Desktop } from "./Components/Desktop";
+import { Crescent } from "./Components/Crescent";
+import { ThemeSwitchRadio } from "./Components/ThemeSwitchRadio/ThemeSwitchRadio";
+import { EThemesNames } from "../../Themes/themesNames";
+import classNames from "classnames/bind";
+
+import styles from "./ThemeSwitch.less";
+
+const cn = classNames.bind(styles);
+
+interface IThemeSwitch {
+ onThemeChange: (newTheme: EThemesNames) => void;
+ currentTheme: string;
+}
+
+export const ThemeSwitch = ({ onThemeChange, currentTheme }: IThemeSwitch) => {
+ return (
+
+
+
}
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+ );
+};
diff --git a/src/Components/ThemeSwitch/ThemeSwitchModal.less b/src/Components/ThemeSwitch/ThemeSwitchModal.less
new file mode 100644
index 000000000..0a64efa55
--- /dev/null
+++ b/src/Components/ThemeSwitch/ThemeSwitchModal.less
@@ -0,0 +1,11 @@
+.icon {
+ display: inline-flex;
+ vertical-align: -4px;
+}
+
+.icon-container {
+ display: inline-flex;
+ vertical-align: -4px;
+ cursor: pointer;
+ gap: 4px;
+}
diff --git a/src/Components/ThemeSwitch/ThemeSwitchModal.tsx b/src/Components/ThemeSwitch/ThemeSwitchModal.tsx
new file mode 100644
index 000000000..47016881a
--- /dev/null
+++ b/src/Components/ThemeSwitch/ThemeSwitchModal.tsx
@@ -0,0 +1,46 @@
+import React, { useEffect } from "react";
+import { Modal } from "@skbkontur/react-ui";
+import { ThemeSwitch } from "./ThemeSwitch";
+import { useDispatch, useSelector } from "react-redux";
+import { useThemeFeature } from "../../hooks/themes/useThemeFeature";
+import { setTheme } from "../../store/Reducers/UIReducer.slice";
+import { UIState } from "../../store/selectors";
+import { EThemesNames } from "../../Themes/themesNames";
+import { useModal } from "../../hooks/useModal";
+import { ThemeSwitchIcon } from "./Components/ThemeSwitchIcon/ThemeSwitchIcon";
+import { useIsBrowserPrefersDarkTheme } from "../../hooks/themes/useIsBrowserPrefersDarkTheme";
+
+export const ThemeSwitchModal: React.FC = () => {
+ const { theme: themeName } = useSelector(UIState);
+ const dispatch = useDispatch();
+
+ const [localThemeName, setLocalTheme] = useThemeFeature();
+ const isBrowserDarkThemeEnabled = useIsBrowserPrefersDarkTheme();
+ const { isModalOpen, closeModal, openModal } = useModal();
+
+ const toggleModal = () => (isModalOpen ? closeModal() : openModal());
+
+ const onThemeChange = (newThemeName: EThemesNames) => {
+ setLocalTheme(newThemeName);
+ dispatch(setTheme(newThemeName));
+ };
+
+ useEffect(() => {
+ onThemeChange(localThemeName);
+ }, [isBrowserDarkThemeEnabled]);
+
+ return (
+ <>
+
+
+ {isModalOpen && (
+
+ Choose Theme
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/src/Components/Token/Token.less b/src/Components/Token/Token.less
index 72429ee67..21c19ff77 100644
--- a/src/Components/Token/Token.less
+++ b/src/Components/Token/Token.less
@@ -1,3 +1,5 @@
+@import "~styles/variables.less";
+
// ToDo: добавить ограничение ширины
.token {
display: inline-flex;
@@ -5,14 +7,13 @@
height: 28px;
border-radius: 2px;
box-sizing: border-box;
- background-color: #e5e5e5;
- color: #333;
+ background-color: var(--background-tertiary);
+ color: var(--text-primary);
font-size: 16px;
&.removable {
position: relative;
padding-right: 22px;
-
}
&.nonexistent {
@@ -21,7 +22,6 @@
border: 1px dashed red;
}
-
&.selectable {
margin: 0;
border: none;
@@ -50,7 +50,7 @@
&::before,
&::after {
- content: '';
+ content: "";
position: absolute;
top: 14px;
right: 4px;
diff --git a/src/Components/Token/Token.tsx b/src/Components/Token/Token.tsx
index 3f251c027..8d5f2664c 100644
--- a/src/Components/Token/Token.tsx
+++ b/src/Components/Token/Token.tsx
@@ -1,6 +1,7 @@
import React from "react";
import { Tooltip } from "@skbkontur/react-ui";
import { TokenType } from "../../helpers/TokenType";
+import { useTheme } from "../../Themes";
import classNames from "classnames/bind";
@@ -17,6 +18,7 @@ type Props = {
export const Token = (props: Props): React.ReactElement => {
const { children, type, onRemove, onClick } = props;
+ const theme = useTheme();
if (type === TokenType.REMOVABLE || type === TokenType.NONEXISTENT) {
const handleRemove = () => {
@@ -30,6 +32,9 @@ export const Token = (props: Props): React.ReactElement => {
pos="bottom center"
>
{
return (
team.id !== currentTeamId && (
-
-
-
+
+ handleSetTeamToTransfer(team)}
+ >
+ {team.name}
+
+
)
);
})}
diff --git a/src/Components/TriggerInfo/TriggerInfo.less b/src/Components/TriggerInfo/TriggerInfo.less
index 5c0a16fcb..a59dbe31f 100644
--- a/src/Components/TriggerInfo/TriggerInfo.less
+++ b/src/Components/TriggerInfo/TriggerInfo.less
@@ -1,5 +1,5 @@
-@import '~styles/variables.less';
-@import '~styles/mixins.less';
+@import "~styles/variables.less";
+@import "~styles/mixins.less";
.header {
display: flex;
@@ -43,7 +43,7 @@
dd {
margin-top: 10px;
margin-left: 0;
- width: calc(~'100% - 150px');
+ width: calc(~"100% - 150px");
&.description {
word-wrap: break-word;
@@ -59,14 +59,14 @@
position: relative;
}
-.copyButton{
+.copyButton {
position: absolute;
top: 5px;
left: -18px;
}
.maintenance-info {
- color: @linkColor
+ color: @linkColor;
}
.exception-explanation {
@@ -86,5 +86,5 @@
padding-left: 10px;
min-width: 288px;
display: flex;
- align-items: center
-}
\ No newline at end of file
+ align-items: center;
+}
diff --git a/src/Components/TriggerInfo/TriggerInfo.tsx b/src/Components/TriggerInfo/TriggerInfo.tsx
index 4669f5c64..b37beb55a 100644
--- a/src/Components/TriggerInfo/TriggerInfo.tsx
+++ b/src/Components/TriggerInfo/TriggerInfo.tsx
@@ -136,15 +136,9 @@ export default function TriggerInfo({
{name != null && name !== "" ? name : "[No name]"}
-
- }
- >
- Edit
-
-
+
}>
+ Edit
+
{
diff --git a/src/Components/TriggerListItem/TriggerListItem.less b/src/Components/TriggerListItem/TriggerListItem.less
index f153c1cb0..cf41a19e9 100644
--- a/src/Components/TriggerListItem/TriggerListItem.less
+++ b/src/Components/TriggerListItem/TriggerListItem.less
@@ -1,12 +1,8 @@
-@import '~styles/variables.less';
-@import '~styles/mixins.less';
+@import "~styles/variables.less";
+@import "~styles/mixins.less";
.row {
display: flex;
-
- &.active {
- background-color: #f6f6f6;
- }
}
.state {
@@ -21,7 +17,7 @@
}
&.active:hover {
- background-color: rgba(0, 0, 0, 0.05);
+ background-color: var(--item-hover);
}
}
@@ -46,7 +42,7 @@
.data {
display: flex;
flex-wrap: wrap;
- width: calc(~'100% - 90px');
+ width: calc(~"100% - 90px");
padding-right: 10px;
box-sizing: border-box;
}
@@ -54,12 +50,12 @@
.header {
padding-left: 13px;
padding-right: 20px;
- width: calc(~'100% - 240px');
+ width: calc(~"100% - 240px");
box-sizing: border-box;
text-decoration: none;
&:hover {
- background-color: rgba(0, 0, 0, 0.05);
+ background-color: var(--item-hover);
}
}
@@ -100,20 +96,12 @@
overflow: hidden;
max-height: 60px;
color: #999;
- -webkit-mask-image: linear-gradient(
- to bottom,
- black 60%,
- transparent
- );
+ -webkit-mask-image: linear-gradient(to bottom, black 60%, transparent);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 100% 80px;
-webkit-mask-position: 0px -15px;
- mask-image: linear-gradient(
- to bottom,
- black 60%,
- transparent
- );
+ mask-image: linear-gradient(to bottom, black 60%, transparent);
mask-repeat: no-repeat;
mask-size: 100% 80px;
mask-position: 0px -15px;
diff --git a/src/Components/TriggerListItem/TriggerListItem.tsx b/src/Components/TriggerListItem/TriggerListItem.tsx
index 40ff7d1ea..d30649dba 100644
--- a/src/Components/TriggerListItem/TriggerListItem.tsx
+++ b/src/Components/TriggerListItem/TriggerListItem.tsx
@@ -17,12 +17,12 @@ import Tabs, { Tab } from "../Tabs/Tabs";
import MetricListView, { SortingColumn } from "../MetricList/MetricList";
import { sanitize } from "dompurify";
import { sortMetrics } from "../../helpers/sort-metrics";
+import _ from "lodash";
import classNames from "classnames/bind";
import styles from "./TriggerListItem.less";
const cn = classNames.bind(styles);
-import _ from "lodash";
type Props = {
data: Trigger;
diff --git a/src/Containers/AllTeamsContainer/AllTeamsContainer.less b/src/Containers/AllTeamsContainer/AllTeamsContainer.less
index b3de906f2..5747978d7 100644
--- a/src/Containers/AllTeamsContainer/AllTeamsContainer.less
+++ b/src/Containers/AllTeamsContainer/AllTeamsContainer.less
@@ -1,6 +1,3 @@
-@import "~styles/variables.less";
-@import "~styles/mixins.less";
-
.teams-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
diff --git a/src/Containers/AllTeamsContainer/AllTeamsContainer.tsx b/src/Containers/AllTeamsContainer/AllTeamsContainer.tsx
index 0dacc0e0e..1bcc81dd2 100644
--- a/src/Containers/AllTeamsContainer/AllTeamsContainer.tsx
+++ b/src/Containers/AllTeamsContainer/AllTeamsContainer.tsx
@@ -14,9 +14,9 @@ import transformPageFromHumanToProgrammer from "../../logic/transformPageFromHum
import { Select } from "@skbkontur/react-ui/components/Select";
import { EmptyListText } from "../../Components/TriggerInfo/Components/EmptyListMessage/EmptyListText";
import { TeamCard } from "../../Components/Teams/TeamCard/TeamCard";
+import { setError } from "../../store/Reducers/UIReducer.slice";
import styles from "./AllTeamsContainer.less";
-import { setError } from "../../store/Reducers/UIReducer.slice";
const cn = classNames.bind(styles);
diff --git a/src/Containers/SettingsContainer.tsx b/src/Containers/SettingsContainer.tsx
index db800264f..a35c5746b 100644
--- a/src/Containers/SettingsContainer.tsx
+++ b/src/Containers/SettingsContainer.tsx
@@ -47,7 +47,6 @@ const SettingsContainer: FC = ({ isTeamMember, history
diff --git a/src/Containers/SubscriptionListContainer/Components/FilterSubscriptionButtons.tsx b/src/Containers/SubscriptionListContainer/Components/FilterSubscriptionButtons.tsx
index e87e6a669..55eeef8ef 100644
--- a/src/Containers/SubscriptionListContainer/Components/FilterSubscriptionButtons.tsx
+++ b/src/Containers/SubscriptionListContainer/Components/FilterSubscriptionButtons.tsx
@@ -2,10 +2,15 @@ import React from "react";
import { Button } from "@skbkontur/react-ui/components/Button";
import { Filter } from "@skbkontur/react-icons";
import { Contact } from "../../../Domain/Contact";
-import { Checkbox, DropdownMenu, MenuItem, ThemeContext, ThemeFactory } from "@skbkontur/react-ui";
+import { Checkbox, DropdownMenu } from "@skbkontur/react-ui";
import ContactInfo from "../../../Components/ContactInfo/ContactInfo";
import { ArrowChevronDown } from "@skbkontur/react-icons";
import TagDropdownSelect from "../../../Components/TagDropdownSelect/TagDropdownSelect";
+import classNames from "classnames/bind";
+
+import styles from "../../../../local_modules/styles/mixins.less";
+
+const cn = classNames.bind(styles);
interface IFilterSubscriptionButtons {
contacts: Contact[];
@@ -47,25 +52,19 @@ export const FilterSubscriptionButtons = ({
<>
{availableContacts.map((contact) => (
-
-
-
+
+ handleFilterContactsChange(contact.id)}
+ >
+
+
+
))}
= ({ children }) => {
+ const theme = useAppTheme();
+
+ return {children};
+};
diff --git a/src/Providers/Providers.tsx b/src/Providers/Providers.tsx
index 3225c3879..25bb8793e 100644
--- a/src/Providers/Providers.tsx
+++ b/src/Providers/Providers.tsx
@@ -3,11 +3,14 @@ import { Provider } from "react-redux";
import { LocaleContext } from "@skbkontur/react-ui/lib/locale/LocaleContext";
import { LangCodes } from "@skbkontur/react-ui/lib/locale";
import { store } from "../store/store";
+import { AppThemeProvider } from "./AppThemeProvider";
export const Providers = ({ children }: { children: React.ReactNode }) => {
return (
- {children}
+
+ {children}
+
);
};
diff --git a/src/Themes/defaultDark.ts b/src/Themes/defaultDark.ts
new file mode 100644
index 000000000..17b6c619e
--- /dev/null
+++ b/src/Themes/defaultDark.ts
@@ -0,0 +1,41 @@
+import { DARK_THEME, ThemeFactory } from "@skbkontur/react-ui";
+import { ApplicationTheme } from "../hooks/themes/useTheme";
+
+export const defaultDark = ThemeFactory.create(
+ {
+ name: "Dark Theme",
+ isDark: true,
+
+ appBgColorSecondary: "var(--background-secondary)",
+ appBgColorTertiary: "var(--background-tertiary)",
+
+ cmGutterColor: "var(--background-gutter)",
+ cmLineNumberColor: "var(--line-number-gutter)",
+ cmActiveLineGutter: "var(--active-line-gutter)",
+ cmActiveLine: "var(--active-line)",
+ cmGutterBorder: "var(--border-gutter)",
+
+ headerMenuButtons: "var(--header-menu-buttons)",
+
+ textColorDefault: "#e6e6e6",
+
+ iconCheckedColor: "var(--icon-checked-color)",
+ iconHoverColor: "var(--icon-hover-color)",
+ iconColor: "var(--icon-color)",
+
+ inputBg: "var(--background-secondary)",
+ textareaBg: "var(--background-secondary)",
+
+ tabColorFocus: "var(--text-primary)",
+ tabColorHover: "var(--text-secondary)",
+
+ sidePageBgDefault: "var(--background-secondary)",
+
+ modalBackBg: "rgb(0, 0, 0)",
+
+ chartGridLinesColor: "#505050",
+
+ teamCardBackgroundColor: "var(--background-secondary)",
+ },
+ DARK_THEME
+);
diff --git a/src/Themes/defaultLight.ts b/src/Themes/defaultLight.ts
new file mode 100644
index 000000000..8b0cbbf4a
--- /dev/null
+++ b/src/Themes/defaultLight.ts
@@ -0,0 +1,25 @@
+import { DEFAULT_THEME, ThemeFactory } from "@skbkontur/react-ui";
+import { ApplicationTheme } from "../hooks/themes/useTheme";
+
+export const defaultLight = ThemeFactory.create(
+ {
+ name: "Light Theme",
+ isDark: false,
+ cmGutterColor: "var(--background-gutter)",
+ cmLineNumberColor: "var(--line-number-gutter)",
+ cmActiveLineGutter: "var(--active-line-gutter)",
+ cmActiveLine: "var(--active-line)",
+ cmGutterBorder: "var(--border-gutter)",
+
+ textColorDefault: "#151515",
+
+ iconCheckedColor: "var(--icon-checked-color)",
+ iconHoverColor: "var(--icon-hover-color)",
+ iconColor: "var(--icon-color)",
+
+ headerMenuButtons: "var(--header-menu-buttons)",
+
+ teamCardBackgroundColor: "var(--background-primary)",
+ },
+ DEFAULT_THEME
+);
diff --git a/src/Themes/index.ts b/src/Themes/index.ts
new file mode 100644
index 000000000..d052037db
--- /dev/null
+++ b/src/Themes/index.ts
@@ -0,0 +1,3 @@
+export * from "../hooks/themes/useTheme";
+export * from "./defaultDark";
+export * from "./defaultLight";
diff --git a/src/Themes/themesNames.ts b/src/Themes/themesNames.ts
new file mode 100644
index 000000000..cec34a9b1
--- /dev/null
+++ b/src/Themes/themesNames.ts
@@ -0,0 +1,5 @@
+export enum EThemesNames {
+ Dark = "Dark Theme",
+ Light = "Light Theme",
+ System = "System Theme",
+}
diff --git a/src/desktop.less b/src/desktop.less
index c596789fc..2a4f4570b 100644
--- a/src/desktop.less
+++ b/src/desktop.less
@@ -5,6 +5,7 @@
display: flex;
flex-direction: column;
min-height: 100vh;
+ color: var(--text-primary);
}
.header,
diff --git a/src/helpers/getChartOptions.ts b/src/helpers/getChartOptions.ts
index f49e2d8e7..d2cee6780 100644
--- a/src/helpers/getChartOptions.ts
+++ b/src/helpers/getChartOptions.ts
@@ -1,6 +1,9 @@
import { EContactEventsInterval } from "../Domain/Contact";
-export const getContactEventsChartOptions = (interval: EContactEventsInterval) => ({
+export const getContactEventsChartOptions = (
+ interval: EContactEventsInterval,
+ gridLinesColor?: string
+) => ({
animation: false,
maxBarThickness: 20,
plugins: {
@@ -23,6 +26,7 @@ export const getContactEventsChartOptions = (interval: EContactEventsInterval) =
scales: {
x: {
+ grid: { color: gridLinesColor },
type: "timeseries",
time: {
tooltipFormat:
@@ -42,6 +46,7 @@ export const getContactEventsChartOptions = (interval: EContactEventsInterval) =
},
},
y: {
+ grid: { color: gridLinesColor },
min: 0,
ticks: {
beginAtZero: true,
@@ -54,7 +59,7 @@ export const getContactEventsChartOptions = (interval: EContactEventsInterval) =
},
});
-export const triggerEventsChartOptions = {
+export const triggerEventsChartOptions = (gridLinesColor?: string) => ({
animation: false,
maxBarThickness: 20,
plugins: {
@@ -66,16 +71,18 @@ export const triggerEventsChartOptions = {
indexAxis: "y",
scales: {
x: {
+ grid: { color: gridLinesColor },
title: {
display: true,
text: "Number of Events",
},
},
y: {
+ grid: { color: gridLinesColor },
title: {
display: true,
text: "Triggers",
},
},
},
-};
+});
diff --git a/src/helpers/trigger-search.ts b/src/helpers/trigger-search.ts
new file mode 100644
index 000000000..5720a934b
--- /dev/null
+++ b/src/helpers/trigger-search.ts
@@ -0,0 +1,38 @@
+import { TokenType } from "./TokenType";
+
+export const searchTokens = (query: string, items: string[]): string[] => {
+ const topMatchItems: string[] = [];
+ const otherItems: string[] = [];
+ const sort = (a: string, b: string) => a.length - b.length;
+
+ const queryLowerCase = query.toLowerCase();
+
+ items.forEach((item) => {
+ const itemLowerCase = item.toLowerCase();
+ const index = itemLowerCase.indexOf(queryLowerCase);
+
+ if (index === -1) {
+ return;
+ }
+
+ if (index === 0) {
+ topMatchItems.push(item);
+ }
+
+ const prevChar = itemLowerCase[index - 1];
+
+ if (prevChar === " " || prevChar === "." || prevChar === "-") {
+ otherItems.push(item);
+ }
+ });
+
+ return [...topMatchItems.sort(sort), ...otherItems.sort(sort)];
+};
+
+export const getTokenType = (token: string, allTags: string[], loading: boolean): TokenType => {
+ if (loading) {
+ return TokenType.REMOVABLE;
+ }
+
+ return allTags.includes(token) ? TokenType.REMOVABLE : TokenType.NONEXISTENT;
+};
diff --git a/src/hooks/themes/useAppThemeDetector.ts b/src/hooks/themes/useAppThemeDetector.ts
new file mode 100644
index 000000000..8c571a838
--- /dev/null
+++ b/src/hooks/themes/useAppThemeDetector.ts
@@ -0,0 +1,21 @@
+import { defaultDark, defaultLight } from "../../Themes";
+import { EThemesNames } from "../../Themes/themesNames";
+import { useAppSelector } from "../../store/hooks";
+import { UIState } from "../../store/selectors";
+import { useIsBrowserPrefersDarkTheme } from "./useIsBrowserPrefersDarkTheme";
+
+export const useAppTheme = () => {
+ const isBrowserDarkThemeEnabled = useIsBrowserPrefersDarkTheme();
+ const { theme } = useAppSelector(UIState);
+
+ switch (theme) {
+ case EThemesNames.Light:
+ return defaultLight;
+ case EThemesNames.Dark:
+ return defaultDark;
+ case EThemesNames.System:
+ return isBrowserDarkThemeEnabled ? defaultDark : defaultLight;
+ default:
+ return isBrowserDarkThemeEnabled ? defaultDark : defaultLight;
+ }
+};
diff --git a/src/hooks/themes/useFeatureFlag.ts b/src/hooks/themes/useFeatureFlag.ts
new file mode 100644
index 000000000..458d9378f
--- /dev/null
+++ b/src/hooks/themes/useFeatureFlag.ts
@@ -0,0 +1,26 @@
+import { Dispatch, SetStateAction, useEffect, useState } from "react";
+import { TFeatureFlag } from "./useThemeFeature";
+
+export const useFeatureFlag = (
+ featureFlag: TFeatureFlag
+): [T, Dispatch>] => {
+ const key = `ff_${featureFlag.id}`;
+
+ const [value, setValue] = useState(() => {
+ const localValue = window.localStorage.getItem(key);
+ if (!localValue) {
+ return featureFlag.defaultValue;
+ }
+ try {
+ return JSON.parse(localValue);
+ } catch (e) {
+ return localValue;
+ }
+ });
+
+ useEffect(() => {
+ window.localStorage.setItem(key, JSON.stringify(value));
+ }, [value]);
+
+ return [value, setValue];
+};
diff --git a/src/hooks/themes/useIsBrowserPrefersDarkTheme.ts b/src/hooks/themes/useIsBrowserPrefersDarkTheme.ts
new file mode 100644
index 000000000..1a8cf2215
--- /dev/null
+++ b/src/hooks/themes/useIsBrowserPrefersDarkTheme.ts
@@ -0,0 +1,29 @@
+import { useEffect, useState } from "react";
+
+export const useIsBrowserPrefersDarkTheme = () => {
+ const getCurrentTheme = () => window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const [isDarkTheme, setIsDarkTheme] = useState(getCurrentTheme());
+
+ const mqListener = (e: MediaQueryListEvent) => {
+ setIsDarkTheme(e.matches);
+ };
+
+ useEffect(() => {
+ const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
+
+ try {
+ darkThemeMq.addEventListener("change", mqListener);
+ } catch (e) {
+ console.warn("Unable to add theme change listener", e);
+ }
+ return () => {
+ try {
+ darkThemeMq.removeEventListener("change", mqListener);
+ } catch (e) {
+ console.warn("Unable to remove theme change listener", e);
+ }
+ };
+ }, []);
+
+ return isDarkTheme;
+};
diff --git a/src/hooks/themes/useTheme.ts b/src/hooks/themes/useTheme.ts
new file mode 100644
index 000000000..12e12fdd9
--- /dev/null
+++ b/src/hooks/themes/useTheme.ts
@@ -0,0 +1,36 @@
+import { ThemeContext } from "@skbkontur/react-ui";
+import { DefaultTheme } from "@skbkontur/react-ui/internal/themes/DefaultTheme";
+import { useContext } from "react";
+import { ThemeIn } from "@skbkontur/react-ui/lib/theming/Theme";
+export interface ApplicationTheme extends ThemeIn {
+ name: string;
+ isDark: boolean;
+
+ appBgColorSecondary?: string;
+ appBgColorTertiary?: string;
+
+ textColorDefault?: string;
+
+ itemHover?: string;
+
+ teamCardBackgroundColor: string;
+
+ iconCheckedColor: string;
+ iconHoverColor: string;
+ iconColor: string;
+
+ headerMenuButtons: string;
+
+ cmGutterColor?: string;
+ cmLineNumberColor?: string;
+ cmActiveLineGutter?: string;
+ cmActiveLine?: string;
+ chartGridLinesColor?: string;
+ cmGutterBorder?: string;
+}
+
+export type TTheme = Readonly & ApplicationTheme>;
+
+export const useTheme = (): TTheme => {
+ return useContext(ThemeContext) as TTheme;
+};
diff --git a/src/hooks/themes/useThemeFeature.ts b/src/hooks/themes/useThemeFeature.ts
new file mode 100644
index 000000000..ed3724ed1
--- /dev/null
+++ b/src/hooks/themes/useThemeFeature.ts
@@ -0,0 +1,35 @@
+import { useEffect } from "react";
+import { EThemesNames } from "../../Themes/themesNames";
+import { useAppSelector } from "../../store/hooks";
+import { useFeatureFlag } from "./useFeatureFlag";
+import { useIsBrowserPrefersDarkTheme } from "./useIsBrowserPrefersDarkTheme";
+import { UIState } from "../../store/selectors";
+
+export type TFeatureFlag = {
+ id: string;
+ label: string;
+ defaultValue: T;
+};
+
+const ThemeFlag: TFeatureFlag = {
+ id: "theme",
+ label: "Application theme",
+ defaultValue: "System Theme",
+};
+
+export const useThemeFeature = () => {
+ const isBrowserDarkThemeEnabled = useIsBrowserPrefersDarkTheme();
+ const { theme } = useAppSelector(UIState);
+ const localValue = theme;
+
+ useEffect(() => {
+ const isSystemTheme = localValue === EThemesNames.System;
+ const browserTheme = isBrowserDarkThemeEnabled ? EThemesNames.Dark : EThemesNames.Light;
+
+ const resultTheme = isSystemTheme ? browserTheme : localValue;
+
+ document.body.dataset.theme = resultTheme;
+ }, [localValue]);
+
+ return useFeatureFlag({ ...ThemeFlag, defaultValue: localValue });
+};
diff --git a/src/pages/trigger-list/trigger-list.desktop.tsx b/src/pages/trigger-list/trigger-list.desktop.tsx
index 88c6205e0..714c0d04d 100644
--- a/src/pages/trigger-list/trigger-list.desktop.tsx
+++ b/src/pages/trigger-list/trigger-list.desktop.tsx
@@ -1,5 +1,4 @@
-import React from "react";
-import { History } from "history";
+import React, { FC } from "react";
import difference from "lodash/difference";
import { Paging } from "@skbkontur/react-ui/components/Paging";
import { Toggle } from "@skbkontur/react-ui/components/Toggle";
@@ -11,6 +10,8 @@ import { SearchSelector } from "../../Components/SearchSelector/SearchSelector";
import AddingButton from "../../Components/AddingButton/AddingButton";
import TriggerList from "../../Components/TriggerList/TriggerList";
import { TriggerListUpdate } from "./trigger-list";
+import { useTheme } from "../../Themes";
+import { useHistory } from "react-router";
export type TriggerListDesktopProps = {
selectedTags: string[];
@@ -26,10 +27,9 @@ export type TriggerListDesktopProps = {
error?: string | null;
onSetMetricMaintenance: (triggerId: string, metric: string, maintenance: number) => void;
onRemoveMetric: (triggerId: string, metric: string) => void;
- history: History;
};
-const TriggerListDesktop: React.FC = ({
+const TriggerListDesktop: FC = ({
selectedTags,
subscribedTags,
allTags,
@@ -43,9 +43,11 @@ const TriggerListDesktop: React.FC = ({
error,
onSetMetricMaintenance,
onRemoveMetric,
- history,
}) => {
- const handlePageChange = (page: number) => {
+ const theme = useTheme();
+ const history = useHistory();
+
+ const handlePageChange = (page: number): void => {
onChange({ page });
window.scrollTo({
top: 0,
@@ -53,11 +55,11 @@ const TriggerListDesktop: React.FC = ({
});
};
- const handleChange = (tags: string[], searchText: string) => {
+ const handleChange = (tags: string[], searchText: string): void => {
onChange({ tags, searchText });
};
- const handleSearch = (searchText: string) => {
+ const handleSearch = (searchText: string): void => {
onChange({ searchText });
};
@@ -77,7 +79,7 @@ const TriggerListDesktop: React.FC = ({
onSearch={handleSearch}
/>
-
+
onChange({ onlyProblems: value })}
@@ -90,7 +92,7 @@ const TriggerListDesktop: React.FC = ({
) {
state.isChristmasMood = action.payload;
},
+ setTheme: (state, { payload }: { payload: EThemesNames }) => {
+ state.theme = payload;
+ },
},
extraReducers: (builder) => {
builder.addMatcher(TriggerApi.endpoints.getTrigger.matchFulfilled, (state, { payload }) => {
@@ -91,6 +97,7 @@ export const {
removeSubscriptionFromTransfer,
toggleSubscriptionTransfer,
toggleChristmasMood,
+ setTheme,
} = UISlice.actions;
export default UISlice.reducer;
diff --git a/webpack.config.js b/webpack.config.js
index 556ee92ae..70589246f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -32,7 +32,7 @@ module.exports = {
clientsClaim: true,
sourcemap: false,
skipWaiting: true,
- exclude: [/index\.html$/, /.*oauth.*/, /\.map$/],
+ exclude: [/\*\.html$/, /.*oauth.*/, /\.map$/],
runtimeCaching: [
{
urlPattern: ({ request }) =>
@@ -43,6 +43,10 @@ module.exports = {
handler: "NetworkFirst",
options: {
cacheName: "html-pages",
+ expiration: {
+ maxEntries: 8,
+ maxAgeSeconds: 5 * 24 * 60 * 60,
+ },
},
},
],
diff --git a/yarn.lock b/yarn.lock
index caa880089..e5c421399 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5068,6 +5068,11 @@ classnames@2.2.6:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
+classnames@2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+ integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+
classnames@^2.2.5:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz"
@@ -8884,7 +8889,7 @@ lodash.invokemap@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz"
integrity sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w==
-lodash.isequal@^4.5.0:
+lodash.isequal@4.5.0, lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
@@ -11728,7 +11733,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -11746,15 +11751,6 @@ string-width@^3.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz"
@@ -12286,7 +12282,7 @@ tsconfig-paths@^3.9.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@^1.13.0, tslib@^1.9.2:
+tslib@^1, tslib@^1.13.0, tslib@^1.9.2:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@@ -12794,7 +12790,7 @@ walker@^1.0.8:
dependencies:
makeerror "1.0.12"
-warning@^4.0.2, warning@^4.0.3:
+warning@4.0.3, warning@^4.0.2, warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
@@ -13292,16 +13288,7 @@ workbox-window@7.1.0:
"@types/trusted-types" "^2.0.2"
workbox-core "7.1.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==