summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorMatain <[email protected]>2022-06-12 23:38:31 +0800
committerMatain <[email protected]>2022-06-12 23:38:31 +0800
commite4e921e0b93f74bf126ca80cbb83f5e912f73a88 (patch)
treeca586f4753f5266ab67051235c7a79370fca1333 /src/components
parenta825925cc97d95762634d234ef06be1627a21fb1 (diff)
parentea5d7cf003eeef30cb7bbe789c6ba7f314bf1ce4 (diff)
Merge branch 'haishanh-master'
Diffstat (limited to 'src/components')
-rw-r--r--src/components/APIConfig.tsx5
-rw-r--r--src/components/APIDiscovery.tsx6
-rw-r--r--src/components/BackendList.tsx13
-rw-r--r--src/components/Button.module.scss10
-rw-r--r--src/components/Button.tsx10
-rw-r--r--src/components/Collapsible.tsx28
-rw-r--r--src/components/CollapsibleSectionHeader.tsx7
-rw-r--r--src/components/Config.tsx165
-rw-r--r--src/components/ConnectionTable.tsx7
-rw-r--r--src/components/Connections.tsx12
-rw-r--r--src/components/Input.tsx17
-rw-r--r--src/components/Logs.tsx3
-rw-r--r--src/components/Root.scss2
-rw-r--r--src/components/Rules.tsx19
-rw-r--r--src/components/Search.tsx7
-rw-r--r--src/components/SideBar.tsx9
-rw-r--r--src/components/StyleGuide.tsx12
-rw-r--r--src/components/SvgYacd.tsx31
-rw-r--r--src/components/ToggleSwitch.tsx5
-rw-r--r--src/components/TrafficChart.tsx13
-rw-r--r--src/components/TrafficNow.module.scss6
-rw-r--r--src/components/about/About.tsx29
-rw-r--r--src/components/proxies/ClosePrevConns.tsx9
-rw-r--r--src/components/proxies/Proxies.tsx20
-rw-r--r--src/components/proxies/Proxy.module.scss36
-rw-r--r--src/components/proxies/Proxy.tsx106
-rw-r--r--src/components/proxies/ProxyGroup.module.scss10
-rw-r--r--src/components/proxies/ProxyGroup.tsx80
-rw-r--r--src/components/proxies/ProxyLatency.tsx4
-rw-r--r--src/components/proxies/ProxyList.module.scss4
-rw-r--r--src/components/proxies/ProxyList.tsx7
-rw-r--r--src/components/proxies/ProxyPageFab.tsx7
-rw-r--r--src/components/proxies/ProxyProvider.module.scss20
-rw-r--r--src/components/proxies/ProxyProvider.tsx84
-rw-r--r--src/components/proxies/ProxyProviderList.tsx6
-rw-r--r--src/components/proxies/Settings.tsx6
-rw-r--r--src/components/proxies/hooks.tsx12
-rw-r--r--src/components/proxies/proxies.hooks.tsx9
-rw-r--r--src/components/rules/RuleProviderItem.module.scss13
-rw-r--r--src/components/rules/RuleProviderItem.tsx20
-rw-r--r--src/components/shared/Fab.tsx12
-rw-r--r--src/components/shared/RotateIcon.tsx9
-rw-r--r--src/components/shared/TextFitler.tsx5
-rw-r--r--src/components/shared/ThemeSwitcher.module.scss3
-rw-r--r--src/components/shared/ZapAnimated.module.scss12
-rw-r--r--src/components/shared/ZapAnimated.tsx25
-rw-r--r--src/components/svg/Equalizer.tsx5
47 files changed, 364 insertions, 576 deletions
diff --git a/src/components/APIConfig.tsx b/src/components/APIConfig.tsx
index 6e11bc4..4a11c92 100644
--- a/src/components/APIConfig.tsx
+++ b/src/components/APIConfig.tsx
@@ -72,9 +72,9 @@ function APIConfig({ dispatch }) {
const detectApiServer = async () => {
// if there is already a clash API server at `/`, just use it as default value
const res = await fetch('/');
- res.json().then(data => {
+ res.json().then((data) => {
if (data['hello'] === 'clash') {
- setBaseURL(window.location.origin)
+ setBaseURL(window.location.origin);
}
});
};
@@ -82,7 +82,6 @@ function APIConfig({ dispatch }) {
detectApiServer();
}, []);
-
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div className={s0.root} ref={contentEl} onKeyDown={handleContentOnKeyDown}>
diff --git a/src/components/APIDiscovery.tsx b/src/components/APIDiscovery.tsx
index f34c886..d211e04 100644
--- a/src/components/APIDiscovery.tsx
+++ b/src/components/APIDiscovery.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import { ThemeSwitcher } from 'src/components/shared/ThemeSwitcher';
-import { DOES_NOT_SUPPORT_FETCH, errors } from 'src/misc/errors';
+import { DOES_NOT_SUPPORT_FETCH, errors, YacdError } from 'src/misc/errors';
import { getClashAPIConfig } from 'src/store/app';
import { fetchConfigs } from 'src/store/configs';
import { closeModal } from 'src/store/modals';
@@ -16,9 +16,7 @@ const { useCallback, useEffect } = React;
function APIDiscovery({ dispatch, apiConfig, modals }) {
if (!window.fetch) {
const { detail } = errors[DOES_NOT_SUPPORT_FETCH];
- const err = new Error(detail);
- // @ts-expect-error ts-migrate(2339) FIXME: Property 'code' does not exist on type 'Error'.
- err.code = DOES_NOT_SUPPORT_FETCH;
+ const err = new YacdError(detail, DOES_NOT_SUPPORT_FETCH);
throw err;
}
diff --git a/src/components/BackendList.tsx b/src/components/BackendList.tsx
index 8e0d906..9ad833c 100644
--- a/src/components/BackendList.tsx
+++ b/src/components/BackendList.tsx
@@ -2,10 +2,7 @@ import cx from 'clsx';
import * as React from 'react';
import { Eye, EyeOff, X as Close } from 'react-feather';
import { useToggle } from 'src/hooks/basic';
-import {
- getClashAPIConfigs,
- getSelectedClashAPIConfigIndex,
-} from 'src/store/app';
+import { getClashAPIConfigs, getSelectedClashAPIConfigIndex } from 'src/store/app';
import { ClashAPIConfig } from 'src/types';
import s from './BackendList.module.scss';
@@ -113,8 +110,6 @@ function Item({
{secret ? (
<>
<span className={s.secret}>{show ? secret : '***'}</span>
-
- {/* @ts-expect-error ts-migrate(2322) FIXME: Type 'boolean | (() => void)' is not assignable to... Remove this comment to see the full error message */}
<Button onClick={toggle} className={s.eye}>
<Icon size={20} />
</Button>
@@ -137,11 +132,7 @@ function Button({
disabled?: boolean;
}) {
return (
- <button
- disabled={disabled}
- className={cx(className, s.btn)}
- onClick={onClick}
- >
+ <button disabled={disabled} className={cx(className, s.btn)} onClick={onClick}>
{children}
</button>
);
diff --git a/src/components/Button.module.scss b/src/components/Button.module.scss
index b46d79c..710d3ad 100644
--- a/src/components/Button.module.scss
+++ b/src/components/Button.module.scss
@@ -23,16 +23,16 @@
transform: scale(0.97);
}
- font-size: 0.75em;
- padding: 4px 7px;
- @media (--breakpoint-not-small) {
- font-size: small;
- padding: 6px 12px;
+ padding: 10px 13px;
+
+ &.circular {
+ padding: 8px;
}
&.minimal {
border-color: transparent;
background: none;
+ padding: 6px 12px;
&:focus {
border-color: var(--color-focus-blue);
}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 1725d1b..8125edc 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -17,7 +17,7 @@ type ButtonProps = {
isLoading?: boolean;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => unknown;
disabled?: boolean;
- kind?: 'primary' | 'minimal';
+ kind?: 'primary' | 'minimal' | 'circular';
className?: string;
title?: string;
} & ButtonInternalProps;
@@ -36,7 +36,7 @@ function Button(props: ButtonProps, ref: React.Ref<HTMLButtonElement>) {
...restProps
} = props;
const internalProps = { children, label, text, start };
- const internalOnClick = useCallback(
+ const internalOnClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(e) => {
if (isLoading) return;
onClick && onClick(e);
@@ -45,7 +45,7 @@ function Button(props: ButtonProps, ref: React.Ref<HTMLButtonElement>) {
);
const btnClassName = cx(
s0.btn,
- { [s0.minimal]: kind === 'minimal' },
+ { [s0.minimal]: kind === 'minimal', [s0.circular]: kind === 'circular' },
className
);
return (
@@ -76,9 +76,7 @@ function ButtonInternal({ children, label, text, start }: ButtonInternalProps) {
return (
<>
{start ? (
- <span className={s0.btnStart}>
- {typeof start === 'function' ? start() : start}
- </span>
+ <span className={s0.btnStart}>{typeof start === 'function' ? start() : start}</span>
) : null}
{children || label || text}
</>
diff --git a/src/components/Collapsible.tsx b/src/components/Collapsible.tsx
index e9a1ee8..65284cd 100644
--- a/src/components/Collapsible.tsx
+++ b/src/components/Collapsible.tsx
@@ -1,17 +1,18 @@
-import React from 'react';
+import type { MutableRefObject } from 'react';
+import * as React from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { framerMotionResouce } from '../misc/motion';
const { memo, useState, useRef, useEffect } = React;
-function usePrevious(value) {
+function usePrevious(value: any) {
const ref = useRef();
useEffect(() => void (ref.current = value), [value]);
return ref.current;
}
-function useMeasure() {
+function useMeasure(): [MutableRefObject<HTMLElement>, { height: number }] {
const ref = useRef();
const [bounds, set] = useState({ height: 0 });
useEffect(() => {
@@ -27,7 +28,7 @@ const variantsCollpapsibleWrap = {
height: 'auto',
transition: { duration: 0 },
},
- open: (height) => ({
+ open: (height: number) => ({
height,
opacity: 1,
visibility: 'visible',
@@ -50,30 +51,21 @@ const variantsCollpapsibleChildContainer = {
},
};
-// @ts-expect-error ts-migrate(2339) FIXME: Property 'isOpen' does not exist on type '{ childr... Remove this comment to see the full error message
-const Collapsible = memo(({ children, isOpen }) => {
+type CollapsibleProps = { children: React.ReactNode; isOpen?: boolean };
+
+const Collapsible = memo(({ children, isOpen }: CollapsibleProps) => {
const module = framerMotionResouce.read();
const motion = module.motion;
const previous = usePrevious(isOpen);
- // @ts-expect-error ts-migrate(2339) FIXME: Property 'height' does not exist on type 'MutableR... Remove this comment to see the full error message
const [refToMeature, { height }] = useMeasure();
return (
<div>
<motion.div
- animate={
- isOpen && previous === isOpen
- ? 'initialOpen'
- : isOpen
- ? 'open'
- : 'closed'
- }
+ animate={isOpen && previous === isOpen ? 'initialOpen' : isOpen ? 'open' : 'closed'}
custom={height}
variants={variantsCollpapsibleWrap}
>
- <motion.div
- variants={variantsCollpapsibleChildContainer}
- ref={refToMeature}
- >
+ <motion.div variants={variantsCollpapsibleChildContainer} ref={refToMeature}>
{children}
</motion.div>
</motion.div>
diff --git a/src/components/CollapsibleSectionHeader.tsx b/src/components/CollapsibleSectionHeader.tsx
index 2d5ecd1..8b701e1 100644
--- a/src/components/CollapsibleSectionHeader.tsx
+++ b/src/components/CollapsibleSectionHeader.tsx
@@ -39,12 +39,7 @@ export default function Header({ name, type, toggle, isOpen, qty }: Props) {
{typeof qty === 'number' ? <span className={s.qty}>{qty}</span> : null}
- <Button
- kind="minimal"
- onClick={toggle}
- className={s.btn}
- title="Toggle collapsible section"
- >
+ <Button kind="minimal" onClick={toggle} className={s.btn} title="Toggle collapsible section">
<span className={cx(s.arrow, { [s.isOpen]: isOpen })}>
<ChevronDown size={20} />
</span>
diff --git a/src/components/Config.tsx b/src/components/Config.tsx
index ded79ad..804e87f 100644
--- a/src/components/Config.tsx
+++ b/src/components/Config.tsx
@@ -7,21 +7,8 @@ import Select from 'src/components/shared/Select';
import { ClashGeneralConfig, DispatchFn, State } from 'src/store/types';
import { ClashAPIConfig } from 'src/types';
-import { fetchVersion } from '$src/api/version';
-
-import {
- getClashAPIConfig,
- getLatencyTestUrl,
- getSelectedChartStyleIndex,
-} from '../store/app';
-import {
- fetchConfigs,
- flushFakeIPPool,
- getConfigs,
- reloadConfigFile,
- updateConfigs,
- updateGeoDatabasesFile,
-} from '../store/configs';
+import { getClashAPIConfig, getLatencyTestUrl, getSelectedChartStyleIndex } from '../store/app';
+import { fetchConfigs, getConfigs, updateConfigs } from '../store/configs';
import { openModal } from '../store/modals';
import Button from './Button';
import s0 from './Config.module.scss';
@@ -119,7 +106,7 @@ function ConfigImpl({
}, [dispatch]);
const setConfigState = useCallback(
- (name, val) => {
+ (name: keyof ClashGeneralConfig, val: ClashGeneralConfig[keyof ClashGeneralConfig]) => {
setConfigStateInternal({ ...configState, [name]: val });
},
[configState]
@@ -169,14 +156,14 @@ function ConfigImpl({
[apiConfig, dispatch, setConfigState, setTunConfigState]
);
- const handleInputOnChange = useCallback(
+ const handleInputOnChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => handleChangeValue(e.target),
[handleChangeValue]
);
const { selectChartStyleIndex, updateAppConfig } = useStoreActions();
- const handleInputOnBlur = useCallback(
+ const handleInputOnBlur = useCallback<React.FocusEventHandler<HTMLInputElement>>(
(e) => {
const target = e.target;
const { name, value } = target;
@@ -232,7 +219,6 @@ function ConfigImpl({
name={f.key}
value={configState[f.key]}
onChange={handleInputOnChange}
- // @ts-expect-error ts-migrate(2322) FIXME: Type '{ name: string; value: any; onChange: (e: an... Remove this comment to see the full error message
onBlur={handleInputOnBlur}
/>
</div>
@@ -243,10 +229,8 @@ function ConfigImpl({
<div className={s0.label}>Mode</div>
<Select
options={modeOptions}
- selected={configState['mode']}
- onChange={(e) =>
- handleChangeValue({ name: 'mode', value: e.target.value })
- }
+ selected={mode}
+ onChange={(e) => handleChangeValue({ name: 'mode', value: e.target.value })}
/>
</div>
@@ -255,137 +239,4 @@ function ConfigImpl({
<Select
options={logLeveOptions}
selected={configState['log-level']}
- onChange={(e) =>
- handleChangeValue({ name: 'log-level', value: e.target.value })
- }
- />
- </div>
-
- <div>
- <div className={s0.label}>{t('allow_lan')}</div>
- <div className={s0.wrapSwitch}>
- <Switch
- name="allow-lan"
- checked={configState['allow-lan']}
- onChange={(value: boolean) =>
- handleChangeValue({ name: 'allow-lan', value: value })
- }
- />
- </div>
- </div>
- { version.meta &&
- <div>
- <div className={s0.label}>{t('tls_sniffing')}</div>
- <div className={s0.wrapSwitch}>
- <Switch
- name="sniffing"
- checked={configState['sniffing']}
- onChange={(value: boolean) =>
- handleChangeValue({ name: 'sniffing', value: value })
- }
- />
- </div>
- </div>}
- </div>
- <div className={s0.sep} >
- <div />
- </div>
- { version.meta &&
- <>
- <div className={s0.section}>
- <div>
- <div className={s0.label}>{t('enable_tun_device')}</div>
- <div className={s0.wrapSwitch}>
- <Switch
- checked={configState['tun']?.enable}
- onChange={(value: boolean) =>
- handleChangeValue({ name: 'enable', value: value })
- }
- />
- </div>
- </div>
- <div>
- <div className={s0.label}>TUN IP Stack</div>
- <Select
- options={tunStackOptions}
- selected={configState['tun']?.stack}
- onChange={(e) =>
- handleChangeValue({ name: 'stack', value: e.target.value })
- }
- />
- </div>
- </div>
- <div className={s0.sep}>
- <div />
- </div>
- <div className={s0.section}>
- <div>
- <div className={s0.label}>Reload</div>
- <Button
- start={<RotateCw size={16} />}
- label={t('reload_config_file')}
- onClick={handleReloadConfigFile} />
- </div>
- <div>
- <div className={s0.label}>GEO Databases</div>
- <Button
- start={<DownloadCloud size={16} />}
- label={t('update_geo_databases_file')}
- onClick={handleUpdateGeoDatabasesFile} />
- </div>
- <div>
- <div className={s0.label}>FakeIP</div>
- <Button
- start={<Trash2 size={16} />}
- label={t('flush_fake_ip_pool')}
- onClick={handleFlushFakeIPPool} />
- </div>
- </div>
- <div className={s0.sep}>
- <div />
- </div>
- </>}
-
- <div className={s0.section}>
- <div>
- <div className={s0.label}>{t('latency_test_url')}</div>
- <SelfControlledInput
- name="latencyTestUrl"
- type="text"
- value={latencyTestUrl}
- onBlur={handleInputOnBlur}
- />
- </div>
- <div>
- <div className={s0.label}>{t('lang')}</div>
- <div>
- <Select
- options={langOptions}
- selected={i18n.language}
- onChange={(e) => i18n.changeLanguage(e.target.value)}
- />
- </div>
- </div>
-
- <div>
- <div className={s0.label}>{t('chart_style')}</div>
- <Selection2
- OptionComponent={TrafficChartSample}
- optionPropsList={propsList}
- selectedIndex={selectedChartStyleIndex}
- onChange={selectChartStyleIndex}
- />
- </div>
-
- <div>
- <div className={s0.label}>Action</div>
- <Button
- start={<LogOut size={16} />}
- label="Switch backend"
- onClick={openAPIConfigModal}
- />
- </div>
- </div>
- </div>
- );
-}
+ onChange={(e) => handleChangeValue({ name: 'log-level', value: e.target.value })}
diff --git a/src/components/ConnectionTable.tsx b/src/components/ConnectionTable.tsx
index a8ad827..879442a 100644
--- a/src/components/ConnectionTable.tsx
+++ b/src/components/ConnectionTable.tsx
@@ -71,11 +71,8 @@ function Table({ data }) {
return (
<div {...headerGroup.getHeaderGroupProps()} className={s.tr}>
{headerGroup.headers.map((column) => (
- <div
- {...column.getHeaderProps(column.getSortByToggleProps())}
- className={s.th}
- >
- <span>{t(column.render('Header'))}</span>
+ <div {...column.getHeaderProps(column.getSortByToggleProps())} className={s.th}>
+ <span>{column.render('Header')}</span>
<span className={s.sortIconContainer}>
{column.isSorted ? (
<span className={column.isSortedDesc ? '' : s.rotate180}>
diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx
index 3769b32..a36031e 100644
--- a/src/components/Connections.tsx
+++ b/src/components/Connections.tsx
@@ -124,7 +124,7 @@ function renderTableOrPlaceholder(conns: FormattedConn[]) {
);
}
-function ConnQty({ qty }) {
+function connQty({ qty }) {
return qty < 100 ? '' + qty : '99+';
}
@@ -194,17 +194,11 @@ function Conn({ apiConfig }) {
<TabList>
<Tab>
<span>{t('Active')}</span>
- <span className={s.connQty}>
- {/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
- <ConnQty qty={filteredConns.length} />
- </span>
+ <span className={s.connQty}>{connQty({ qty: filteredConns.length })}</span>
</Tab>
<Tab>
<span>{t('Closed')}</span>
- <span className={s.connQty}>
- {/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
- <ConnQty qty={filteredClosedConns.length} />
- </span>
+ <span className={s.connQty}>{connQty({ qty: filteredClosedConns.length })}</span>
</Tab>
</TabList>
<div className={s.inputWrapper}>
diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index c132a3b..efb5665 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -7,7 +7,8 @@ const { useState, useRef, useEffect, useCallback } = React;
type InputProps = {
value?: string | number;
type?: string;
- onChange?: (...args: any[]) => any;
+ onChange?: React.ChangeEventHandler<HTMLInputElement>;
+ onBlur?: React.FocusEventHandler<HTMLInputElement>;
name?: string;
placeholder?: string;
};
@@ -26,17 +27,7 @@ export function SelfControlledInput({ value, ...restProps }) {
}
refValue.current = value;
}, [value]);
- const onChange = useCallback(
- (e) => setInternalValue(e.target.value),
- [setInternalValue]
- );
+ const onChange = useCallback((e) => setInternalValue(e.target.value), [setInternalValue]);
- return (
- <input
- className={s0.input}
- value={internalValue}
- onChange={onChange}
- {...restProps}
- />
- );
+ return <input className={s0.input} value={internalValue} onChange={onChange} {...restProps} />;
}
diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx
index 1dd00a8..003ff0d 100644
--- a/src/components/Logs.tsx
+++ b/src/components/Logs.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
import { Pause, Play } from 'react-feather';
import { useTranslation } from 'react-i18next';
-import { fetchLogs, reconnect as reconnectLogs,stop as stopLogs } from 'src/api/logs';
+import { areEqual, FixedSizeList as List, ListChildComponentProps } from 'react-window';
+import { fetchLogs, reconnect as reconnectLogs, stop as stopLogs } from 'src/api/logs';
import ContentHeader from 'src/components/ContentHeader';
import LogSearch from 'src/components/LogSearch';
import { connect, useStoreActions } from 'src/components/StateProvider';
diff --git a/src/components/Root.scss b/src/components/Root.scss
index 8a7a57c..0a7fc4f 100644
--- a/src/components/Root.scss
+++ b/src/components/Root.scss
@@ -90,6 +90,7 @@ body {
--color-background: #202020;
--color-background2: rgba(32, 32, 32, 0.3);
--color-bg-card: #2d2d2d;
+ --card-hover-border-lightness: 30%;
--color-text: #ddd;
--color-text-secondary: #ccc;
--color-text-highlight: #fff;
@@ -118,6 +119,7 @@ body {
--color-background: #eee;
--color-background2: rgba(240, 240, 240, 0.3);
--color-bg-card: #fafafa;
+ --card-hover-border-lightness: 80%;
--color-text: #222;
--color-text-secondary: #646464;
--color-text-highlight: #040404;
diff --git a/src/components/Rules.tsx b/src/components/Rules.tsx
index e105edf..4b17a4b 100644
--- a/src/components/Rules.tsx
+++ b/src/components/Rules.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { areEqual, VariableSizeList } from 'react-window';
import { RuleProviderItem } from 'src/components/rules/RuleProviderItem';
@@ -7,7 +7,7 @@ import { RulesPageFab } from 'src/components/rules/RulesPageFab';
import { TextFilter } from 'src/components/shared/TextFitler';
import { ruleFilterText } from 'src/store/rules';
import { State } from 'src/store/types';
-import { ClashAPIConfig } from 'src/types';
+import { ClashAPIConfig, RuleType } from 'src/types';
import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
import { getClashAPIConfig } from '../store/app';
@@ -41,15 +41,24 @@ function getItemSizeFactory({ provider }) {
const providerQty = provider.names.length;
if (idx < providerQty) {
// provider
- return 90;
+ return 110;
}
// rule
return 60;
};
}
-// @ts-expect-error ts-migrate(2339) FIXME: Property 'index' does not exist on type '{ childre... Remove this comment to see the full error message
-const Row = memo(({ index, style, data }) => {
+type RowProps = {
+ index: number;
+ style: React.CSSProperties;
+ data: {
+ apiConfig: ClashAPIConfig;
+ rules: RuleType[];
+ provider: { names: string[]; byName: any };
+ };
+};
+
+const Row = memo(({ index, style, data }: RowProps) => {
const { rules, provider, apiConfig } = data;
const providerQty = provider.names.length;
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
index 6edc4a5..9762d8f 100644
--- a/src/components/Search.tsx
+++ b/src/components/Search.tsx
@@ -25,12 +25,7 @@ function RuleSearch({ dispatch, searchText, updateSearchText }) {
<div className={s0.RuleSearch}>
<div className={s0.RuleSearchContainer}>
<div className={s0.inputWrapper}>
- <input
- type="text"
- value={text}
- onChange={onChange}
- className={s0.input}
- />
+ <input type="text" value={text} onChange={onChange} className={s0.input} />
</div>
<div className={s0.iconWrapper}>
<SearchIcon size={20} />
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx
index dbe7f0d..2c5ba0c 100644
--- a/src/components/SideBar.tsx
+++ b/src/components/SideBar.tsx
@@ -3,14 +3,7 @@ import cx from 'clsx';
import * as React from 'react';
import { Info } from 'react-feather';
import { useTranslation } from 'react-i18next';
-import {
- FcAreaChart,
- FcDocument,
- FcGlobe,
- FcLink,
- FcRuler,
- FcSettings,
-} from 'react-icons/fc';
+import { FcAreaChart, FcDocument, FcGlobe, FcLink, FcRuler, FcSettings } from 'react-icons/fc';
import { Link, useLocation } from 'react-router-dom';
import { ThemeSwitcher } from 'src/components/shared/ThemeSwitcher';
diff --git a/src/components/StyleGuide.tsx b/src/components/StyleGuide.tsx
index ee38697..910c538 100644
--- a/src/components/StyleGuide.tsx
+++ b/src/components/StyleGuide.tsx
@@ -4,6 +4,7 @@ import Loading from 'src/components/Loading';
import Button from './Button';
import Input from './Input';
+import { ZapAnimated } from './shared/ZapAnimated';
import SwitchThemed from './SwitchThemed';
import ToggleSwitch from './ToggleSwitch';
@@ -21,7 +22,9 @@ const optionsRule = [
{ label: 'Direct', value: 'Direct' },
];
-const Pane = ({ children, style }) => <div style={{ ...paneStyle, ...style }}>{children}</div>;
+const Pane = ({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) => (
+ <div style={{ ...paneStyle, ...style }}>{children}</div>
+);
function useToggle(initialState = false) {
const [onoff, setonoff] = React.useState(initialState);
@@ -40,19 +43,18 @@ class StyleGuide extends PureComponent {
render() {
return (
<div>
- {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */}
+ <Pane>
+ <ZapAnimated />
+ </Pane>
<Pane>
<SwitchExample />
</Pane>
- {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */}
<Pane>
<Input />
</Pane>
- {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */}
<Pane>
<ToggleSwitch name="test" options={optionsRule} value="Rule" onChange={noop} />
</Pane>
- {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */}
<Pane>
<Button text="Test Lxatency" start={<Zap size={16} />} />
<Button text="Test Lxatency" start={<Zap size={16} />} isLoading />
diff --git a/src/components/SvgYacd.tsx b/src/components/SvgYacd.tsx
index 49455e3..803936c 100644
--- a/src/components/SvgYacd.tsx
+++ b/src/components/SvgYacd.tsx
@@ -23,19 +23,24 @@ function SvgYacd({
}: Props) {
const faceClasName = cx({ [s.path]: animate });
return (
- <svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 512 512" width={width} height={height}>
- <path id="Layer" className={faceClasName} fill={c0} stroke={line} strokeLinecap="round" strokeWidth="4"
- d="m280.8 182.4l119-108.3c1.9-1.7 4.3-2.7 6.8-2.4l39.5 4.1c2.1 0.3 3.9 2.2 3.9 4.4v251.1c0 2-1.5 3.9-3.5 4.4l-41.9 9c-0.5 0.3-1.2 0.3-1.9 0.3h-18.8c-2.4 0-4.4-2-4.4-4.4v-132.9c0-7.5-9-11.7-14.8-6.3l-59 53.4c-2.2 2.2-5.4 2.9-8.5 1.9-27.1-8-56.3-8-83.4 0-2.9 1-6.1 0.3-8.5-1.9l-59-53.4c-5.6-5.4-14.6-1.2-14.6 6.3v132.9c0 2.4-2.2 4.4-4.7 4.4h-18.7c-0.7 0-1.2 0-2-0.3l-41.6-9c-2-0.5-3.5-2.4-3.5-4.4v-251.1c0-2.2 1.8-4.1 3.9-4.4l39.5-4.1c2.5-0.3 4.9 0.7 6.9 2.4l115.7 105.3c2 1.7 4.6 2.5 7.1 2.2 15.3-2.2 31.4-1.9 46.5 0.8z"/>
- <path id="Layer" className={faceClasName} fill={c0} stroke={line} strokeLinecap="round" strokeWidth="4"
- d="m269.4 361.8l-7.1 13.4c-2.4 4.2-8.5 4.2-11 0l-7-13.4c-2.5-4.1 0.7-9.3 5.3-9h14.4c4.9 0 7.8 4.9 5.4 9z"/>
- <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4"
- d="m160.7 362.5c3.6 0 6.8 3.2 6.8 6.9 0 3.6-3.2 6.5-6.8 6.5h-94.6c-3.6 0-6.8-2.9-6.8-6.5 0-3.7 3.2-6.9 6.8-6.9z" />
- <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4"
- d="m158.7 394.7c3.4-1 7.1 1 8.3 4.4 1 3.4-1 7.3-4.4 8.3l-92.8 31.7c-3.4 1.2-7.3-0.7-8.3-4.2-1.2-3.6 0.7-7.3 4.4-8.5z" />
- <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4"
- d="m446.1 426.4c3.4 1.2 5.3 4.9 4.3 8.5-1.2 3.5-4.8 5.4-8.2 4.2l-93.1-31.7c-3.5-1-5.4-4.9-4.2-8.3 1-3.4 4.9-5.4 8.3-4.4z" />
- <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4"
- d="m445.8 362.5c3.7 0 6.6 3.2 6.6 6.9 0 3.6-2.9 6.5-6.6 6.5h-94.8c-3.6 0-6.6-2.9-6.6-6.5 0-3.7 3-6.9 6.6-6.9z" />
+ <svg width={width} height={height} viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg">
+ <g fill="none" fillRule="evenodd">
+ {/* face */}
+ <path
+ d="M71.689 53.055c9.23-1.487 25.684 27.263 41.411 56.663 18.572-8.017 71.708-7.717 93.775 0 4.714-15.612 31.96-57.405 41.626-56.663 3.992.088 13.07 31.705 23.309 94.96 2.743 16.949 7.537 47.492 14.38 91.63-42.339 17.834-84.37 26.751-126.095 26.751-41.724 0-83.756-8.917-126.095-26.751C52.973 116.244 65.536 54.047 71.689 53.055z"
+ stroke={stroke}
+ strokeWidth="4"
+ strokeLinecap="round"
+ fill={c0}
+ className={faceClasName}
+ />
+ <circle fill={eye} cx="216.5" cy="181.5" r="14.5" />
+ <circle fill={eye} cx="104.5" cy="181.5" r="14.5" />
+ {/* mouth */}
+ <g stroke={mouth} strokeLinecap="round" strokeWidth="4">
+ <path d="M175.568 218.694c-2.494 1.582-5.534 2.207-8.563 1.508-3.029-.7-5.487-2.594-7.035-5.11M143.981 218.694c2.494 1.582 5.534 2.207 8.563 1.508 3.03-.7 5.488-2.594 7.036-5.11" />
+ </g>
+ </g>
</svg>
);
}
diff --git a/src/components/ToggleSwitch.tsx b/src/components/ToggleSwitch.tsx
index 9eb1019..58400c9 100644
--- a/src/components/ToggleSwitch.tsx
+++ b/src/components/ToggleSwitch.tsx
@@ -10,10 +10,7 @@ type Props = {
};
function ToggleSwitch({ options, value, name, onChange }: Props) {
- const idxSelected = useMemo(
- () => options.map((o) => o.value).indexOf(value),
- [options, value]
- );
+ const idxSelected = useMemo(() => options.map((o) => o.value).indexOf(value), [options, value]);
const getPortionPercentage = useCallback(
(idx: number) => {
diff --git a/src/components/TrafficChart.tsx b/src/components/TrafficChart.tsx
index 0d67ae0..588049d 100644
--- a/src/components/TrafficChart.tsx
+++ b/src/components/TrafficChart.tsx
@@ -1,21 +1,17 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
-import { State } from '$src/store/types';
+import { State } from '$src/store/types';
import { fetchData } from '../api/traffic';
import useLineChart from '../hooks/useLineChart';
-import {
- chartJSResource,
- chartStyles,
- commonDataSetProps,
-} from '../misc/chart';
+import { chartJSResource, chartStyles, commonDataSetProps } from '../misc/chart';
import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app';
import { connect } from './StateProvider';
const { useMemo } = React;
-const chartWrapperStyle = {
+const chartWrapperStyle: React.CSSProperties = {
// make chartjs chart responsive
position: 'relative',
maxWidth: 1000,
@@ -51,13 +47,12 @@ function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
},
],
}),
- [ traffic, selectedChartStyleIndex, t]
+ [traffic, selectedChartStyleIndex, t]
);
useLineChart(ChartMod.Chart, 'trafficChart', data, traffic);
return (
- // @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message
<div style={chartWrapperStyle}>
<canvas id="trafficChart" />
</div>
diff --git a/src/components/TrafficNow.module.scss b/src/components/TrafficNow.module.scss
index 4d47ad2..d86b993 100644
--- a/src/components/TrafficNow.module.scss
+++ b/src/components/TrafficNow.module.scss
@@ -6,10 +6,12 @@
justify-content: space-between;
max-width: 1000px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, 180px);
+ grid-gap: 10px;
+
.sec {
padding: 10px;
- width: 19%;
- margin: 3px;
background-color: var(--color-bg-card);
border-radius: 10px;
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1);
diff --git a/src/components/about/About.tsx b/src/components/about/About.tsx
index 864e42c..f59ce20 100644
--- a/src/components/about/About.tsx
+++ b/src/components/about/About.tsx
@@ -11,15 +11,7 @@ import s from './About.module.scss';
type Props = { apiConfig: ClashAPIConfig };
-function Version({
- name,
- link,
- version,
-}: {
- name: string;
- link: string;
- version: string;
-}) {
+function Version({ name, link, version }: { name: string; link: string; version: string }) {
return (
<div className={s.root}>
<h2>{name}</h2>
@@ -28,12 +20,7 @@ function Version({
<span className={s.mono}>{version}</span>
</p>
<p>
- <a
- className={s.link}
- href={link}
- target="_blank"
- rel="noopener noreferrer"
- >
+ <a className={s.link} href={link} target="_blank" rel="noopener noreferrer">
<GitHub size={20} />
<span>Source</span>
</a>
@@ -50,17 +37,9 @@ function AboutImpl(props: Props) {
<>
<ContentHeader title="About" />
{version && version.version ? (
- <Version
- name={version.meta?'Clash.Meta':'Clash'}
- version={version.version}
- link="https://github.com/metacubex/clash.meta"
- />
+ <Version name="Clash" version={version.version} link="https://github.com/Dreamacro/clash" />
) : null}
- <Version
- name="Yacd"
- version={__VERSION__}
- link="https://github.com/metacubex/yacd"
- />
+ <Version name="Yacd" version={__VERSION__} link="https://github.com/haishanh/yacd" />
</>
);
}
diff --git a/src/components/proxies/ClosePrevConns.tsx b/src/components/proxies/ClosePrevConns.tsx
index 5617efe..f26a5e9 100644
--- a/src/components/proxies/ClosePrevConns.tsx
+++ b/src/components/proxies/ClosePrevConns.tsx
@@ -10,10 +10,7 @@ type Props = {
onClickSecondaryButton?: () => void;
};
-export function ClosePrevConns({
- onClickPrimaryButton,
- onClickSecondaryButton,
-}: Props) {
+export function ClosePrevConns({ onClickPrimaryButton, onClickSecondaryButton }: Props) {
const primaryButtonRef = useRef<HTMLButtonElement>(null);
const secondaryButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
@@ -33,8 +30,8 @@ export function ClosePrevConns({
<div onKeyDown={handleKeyDown}>
<h2>Close Connections?</h2>
<p>
- Click "Yes" to close those connections that are still using the old
- selected proxy in this group
+ Click "Yes" to close those connections that are still using the old selected proxy in this
+ group
</p>
<div style={{ height: 30 }} />
<FlexCenter>
diff --git a/src/components/proxies/Proxies.tsx b/src/components/proxies/Proxies.tsx
index c1606dd..12d3cb2 100644
--- a/src/components/proxies/Proxies.tsx
+++ b/src/components/proxies/Proxies.tsx
@@ -35,9 +35,7 @@ function Proxies({
apiConfig,
showModalClosePrevConns,
}) {
- const refFetchedTimestamp = useRef<{ startAt?: number; completeAt?: number }>(
- {}
- );
+ const refFetchedTimestamp = useRef<{ startAt?: number; completeAt?: number }>({});
const fetchProxiesHooked = useCallback(() => {
refFetchedTimestamp.current.startAt = Date.now();
@@ -75,10 +73,7 @@ function Proxies({
return (
<>
- <BaseModal
- isOpen={isSettingsModalOpen}
- onRequestClose={closeSettingsModal}
- >
+ <BaseModal isOpen={isSettingsModalOpen} onRequestClose={closeSettingsModal}>
<Settings />
</BaseModal>
<div className={s0.topBar}>
@@ -110,15 +105,8 @@ function Proxies({
</div>
<ProxyProviderList items={proxyProviders} />
<div style={{ height: 60 }} />
- <ProxyPageFab
- dispatch={dispatch}
- apiConfig={apiConfig}
- proxyProviders={proxyProviders}
- />
- <BaseModal
- isOpen={showModalClosePrevConns}
- onRequestClose={closeModalClosePrevConns}
- >
+ <ProxyPageFab dispatch={dispatch} apiConfig={apiConfig} proxyProviders={proxyProviders} />
+ <BaseModal isOpen={showModalClosePrevConns} onRequestClose={closeModalClosePrevConns}>
<ClosePrevConns
onClickPrimaryButton={() => closePrevConnsAndTheModal(apiConfig)}
onClickSecondaryButton={closeModalClosePrevConns}
diff --git a/src/components/proxies/Proxy.module.scss b/src/components/proxies/Proxy.module.scss
index 4507a07..72087cf 100644
--- a/src/components/proxies/Proxy.module.scss
+++ b/src/components/proxies/Proxy.module.scss
@@ -15,7 +15,7 @@
border: 1px solid var(--color-focus-blue);
}
- max-width: 280px;
+ max-width: 200px;
@media (--breakpoint-not-small) {
min-width: 200px;
border-radius: 10px;
@@ -34,7 +34,7 @@
transition: transform 0.2s ease-in-out;
cursor: pointer;
&:hover {
- transform: translateY(-2px);
+ border-color: hsl(0deg, 0%, var(--card-hover-border-lightness));
}
}
}
@@ -58,19 +58,37 @@
width: 100%;
margin-bottom: 5px;
font-size: 0.85em;
- @media (--breakpoint-not-small) {
- font-size: 0.85em;
- }
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.proxySmall {
- width: 11px;
- height: 11px;
+ --size: 13px;
+ width: var(--size);
+ height: var(--size);
border-radius: 50%;
- border: 1px solid var(--color-background);
+ position: relative;
&.now {
- border-color: var(--color-text-secondary);
+ --size: 15px;
+ &:before {
+ --size-dot: 7px;
+ content: '';
+ position: absolute;
+ width: var(--size-dot);
+ height: var(--size-dot);
+ background-color: #fff;
+ // For non-primitive proxy type like "Selector", "LoadBalance", "DIRECT", etc. we are using a transparent
+ // background, and this selected indicator has a white background. In "light" them mode, the constrast
+ // between the bg of the indicator and the "background" is too small. In that case we want to add a
+ // border around this indicator so it's more distinguishable.
+ border: 1px solid var(--color-proxy-dot-selected-ind-bo);
+ border-radius: 4px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
}
&.selectable {
diff --git a/src/components/proxies/Proxy.tsx b/src/components/proxies/Proxy.tsx
index 424d320..47a3d54 100644
--- a/src/components/proxies/Proxy.tsx
+++ b/src/components/proxies/Proxy.tsx
@@ -1,6 +1,8 @@
+import { TooltipPopup, useTooltip } from '@reach/tooltip';
import cx from 'clsx';
import * as React from 'react';
-import { keyCodes } from 'src/misc/keycode';
+
+import { State } from '$src/store/types';
import { getDelay, getProxies, NonProxyTypes } from '../../store/proxies';
import { connect } from '../StateProvider';
@@ -20,11 +22,7 @@ const colorMap = {
na: '#909399',
};
-function getLabelColor({
- number,
-}: {
- number?: number;
-} = {}) {
+function getLabelColor({ number }: { number?: number } = {}) {
if (number === 0) {
return colorMap.na;
} else if (number < 200) {
@@ -37,39 +35,25 @@ function getLabelColor({
return colorMap.na;
}
-function getProxyDotBackgroundColor(
- latency: {
- number?: number;
- },
- proxyType: string
-) {
+function getProxyDotStyle(latency: { number?: number }, proxyType: string) {
if (NonProxyTypes.indexOf(proxyType) > -1) {
- return 'linear-gradient(135deg, white 15%, #999 15% 30%, white 30% 45%, #999 45% 60%, white 60% 75%, #999 75% 90%, white 90% 100%)';
+ return { border: '1px dotted #777' };
}
- return getLabelColor(latency);
+ const bg = getLabelColor(latency);
+ return { background: bg };
}
type ProxyProps = {
name: string;
now?: boolean;
proxy: any;
- latency: any;
+ latency?: { number?: number };
isSelectable?: boolean;
onClick?: (proxyName: string) => unknown;
};
-function ProxySmallImpl({
- now,
- name,
- proxy,
- latency,
- isSelectable,
- onClick,
-}: ProxyProps) {
- const color = useMemo(() => getProxyDotBackgroundColor(latency, proxy.type), [
- latency,
- proxy,
- ]);
+function ProxySmallImpl({ now, name, proxy, latency, isSelectable, onClick }: ProxyProps) {
+ const style = useMemo(() => getProxyDotStyle(latency, proxy.type), [latency, proxy]);
const title = useMemo(() => {
let ret = name;
if (latency && typeof latency.number === 'number') {
@@ -83,17 +67,12 @@ function ProxySmallImpl({
}, [name, onClick, isSelectable]);
const className = useMemo(() => {
- return cx(s0.proxySmall, {
- [s0.now]: now,
- [s0.selectable]: isSelectable,
- });
+ return cx(s0.proxySmall, { [s0.now]: now, [s0.selectable]: isSelectable });
}, [isSelectable, now]);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent) => {
- if (e.keyCode === keyCodes.Enter) {
- doSelect();
- }
+ if (e.key === 'Enter') doSelect();
},
[doSelect]
);
@@ -102,7 +81,7 @@ function ProxySmallImpl({
<div
title={title}
className={className}
- style={{ background: color }}
+ style={style}
onClick={doSelect}
onKeyDown={handleKeyDown}
role={isSelectable ? 'menuitem' : ''}
@@ -115,33 +94,46 @@ function formatProxyType(t: string) {
return t;
}
-function ProxyImpl({
- now,
- name,
- proxy,
- latency,
- isSelectable,
- onClick,
-}: ProxyProps) {
+const positionProxyNameTooltip = (triggerRect: { left: number; top: number }) => {
+ return {
+ left: triggerRect.left + window.scrollX - 5,
+ top: triggerRect.top + window.scrollY - 38,
+ };
+};
+
+function ProxyNameTooltip({ children, label, 'aria-label': ariaLabel }) {
+ const [trigger, tooltip] = useTooltip();
+ return (
+ <>
+ {React.cloneElement(children, trigger)}
+ <TooltipPopup
+ {...tooltip}
+ label={label}
+ aria-label={ariaLabel}
+ position={positionProxyNameTooltip}
+ />
+ </>
+ );
+}
+
+function ProxyImpl({ now, name, proxy, latency, isSelectable, onClick }: ProxyProps) {
const color = useMemo(() => getLabelColor(latency), [latency]);
const doSelect = React.useCallback(() => {
isSelectable && onClick && onClick(name);
}, [name, onClick, isSelectable]);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent) => {
- if (e.keyCode === keyCodes.Enter) {
- doSelect();
- }
+ if (e.key === 'Enter') doSelect();
},
[doSelect]
);
const className = useMemo(() => {
return cx(s0.proxy, {
[s0.now]: now,
- [s0.error]: latency && latency.error,
+ // [s0.error]: latency && latency.error,
[s0.selectable]: isSelectable,
});
- }, [isSelectable, now, latency]);
+ }, [isSelectable, now]);
return (
<div
@@ -151,26 +143,26 @@ function ProxyImpl({
onKeyDown={handleKeyDown}
role={isSelectable ? 'menuitem' : ''}
>
- <div className={s0.proxyName}>{name}</div>
+ <div className={s0.proxyName}>
+ <ProxyNameTooltip label={name} aria-label={'proxy name: ' + name}>
+ <span>{name}</span>
+ </ProxyNameTooltip>
+ </div>
<div className={s0.row}>
<span className={s0.proxyType} style={{ opacity: now ? 0.6 : 0.2 }}>
{formatProxyType(proxy.type)}
</span>
- {latency && latency.number ? (
- <ProxyLatency number={latency.number} color={color} />
- ) : null}
+ <ProxyLatency number={latency?.number} color={color} />
</div>
</div>
);
}
-const mapState = (s: any, { name }) => {
+const mapState = (s: State, { name }) => {
const proxies = getProxies(s);
const delay = getDelay(s);
- return {
- proxy: proxies[name],
- latency: delay[name],
- };
+ const proxy = proxies[name] || { name, type: 'Unknown', history: [] };
+ return { proxy, latency: delay[name] };
};
export const Proxy = connect(mapState)(ProxyImpl);
diff --git a/src/components/proxies/ProxyGroup.module.scss b/src/components/proxies/ProxyGroup.module.scss
index 5409ea8..85b68b6 100644
--- a/src/components/proxies/ProxyGroup.module.scss
+++ b/src/components/proxies/ProxyGroup.module.scss
@@ -2,10 +2,12 @@
margin-bottom: 12px;
}
-.zapWrapper {
- width: 20px;
- height: 20px;
+.groupHead {
display: flex;
+ flex-wrap: wrap;
align-items: center;
- justify-content: center;
+}
+
+.action {
+ margin: 0 5px;
}
diff --git a/src/components/proxies/ProxyGroup.tsx b/src/components/proxies/ProxyGroup.tsx
index 633a4b9..e8ebadf 100644
--- a/src/components/proxies/ProxyGroup.tsx
+++ b/src/components/proxies/ProxyGroup.tsx
@@ -1,30 +1,37 @@
+import Tooltip from '@reach/tooltip';
import * as React from 'react';
-import { Zap } from 'react-feather';
-import { useQuery } from 'react-query';
-import * as proxiesAPI from '$src/api/proxies';
-import { fetchVersion } from '$src/api/version';
-import { getCollapsibleIsOpen, getHideUnavailableProxies, getProxySortBy } from '$src/store/app';
-import { fetchProxies, getProxies, switchProxy } from '$src/store/proxies';
+import { useState2 } from '$src/hooks/basic';
+import { DelayMapping, DispatchFn, ProxiesMapping, State } from '$src/store/types';
+import { ClashAPIConfig } from '$src/types';
+import { getCollapsibleIsOpen, getHideUnavailableProxies, getProxySortBy } from '../../store/app';
+import { fetchProxies, getProxies, switchProxy } from '../../store/proxies';
import Button from '../Button';
import CollapsibleSectionHeader from '../CollapsibleSectionHeader';
+import { ZapAnimated } from '../shared/ZapAnimated';
import { connect, useStoreActions } from '../StateProvider';
import { useFilteredAndSorted } from './hooks';
import s0 from './ProxyGroup.module.scss';
import { ProxyList, ProxyListSummaryView } from './ProxyList';
+import { fetchVersion } from '$src/api/version';
+import { useQuery } from 'react-query';
+const { createElement, useCallback, useMemo } = React;
-const { createElement, useCallback, useMemo, useState } = React;
-
-
-function ZapWrapper() {
- return (
- <div className={s0.zapWrapper}>
- <Zap size={16} />
- </div>
- );
-}
+type ProxyGroupImplProps = {
+ name: string;
+ all: string[];
+ delay: DelayMapping;
+ hideUnavailableProxies: boolean;
+ proxySortBy: string;
+ proxies: ProxiesMapping;
+ type: string;
+ now: string;
+ isOpen: boolean;
+ apiConfig: ClashAPIConfig;
+ dispatch: DispatchFn;
+};
function ProxyGroupImpl({
name,
@@ -38,14 +45,8 @@ function ProxyGroupImpl({
isOpen,
apiConfig,
dispatch,
-}) {
- const all = useFilteredAndSorted(
- allItems,
- delay,
- hideUnavailableProxies,
- proxySortBy,
- proxies
- );
+}: ProxyGroupImplProps) {
+ const all = useFilteredAndSorted(allItems, delay, hideUnavailableProxies, proxySortBy, proxies);
const { data: version } = useQuery(['/version', apiConfig], () =>
fetchVersion('/version',apiConfig)
@@ -63,15 +64,17 @@ function ProxyGroupImpl({
}, [isOpen, updateCollapsibleIsOpen, name]);
const itemOnTapCallback = useCallback(
- (proxyName) => {
+ (proxyName: string) => {
if (!isSelectable) return;
dispatch(switchProxy(apiConfig, name, proxyName));
},
[apiConfig, dispatch, name, isSelectable]
);
- const [isTestingLatency, setIsTestingLatency] = useState(false);
+
+ const testingLatency = useState2(false);
const testLatency = useCallback(async () => {
- setIsTestingLatency(true);
+ if (testingLatency.value) return;
+ testingLatency.set(true);
try {
if (version.meta==true){
await proxiesAPI.requestDelayForProxyGroup(apiConfig,name);
@@ -87,7 +90,7 @@ function ProxyGroupImpl({
return (
<div className={s0.group}>
- <div style={{ display: 'flex', alignItems: 'center' }}>
+ <div className={s0.groupHead}>
<CollapsibleSectionHeader
name={name}
type={type}
@@ -95,14 +98,13 @@ function ProxyGroupImpl({
qty={all.length}
isOpen={isOpen}
/>
- <Button
- title="Test latency"
- kind="minimal"
- onClick={testLatency}
- isLoading={isTestingLatency}
- >
- <ZapWrapper />
- </Button>
+ <div className={s0.action}>
+ <Tooltip label={'Test latency'}>
+ <Button kind="circular" onClick={testLatency}>
+ <ZapAnimated animate={testingLatency.value} size={16} />
+ </Button>
+ </Tooltip>
+ </div>
</div>
{createElement(isOpen ? ProxyList : ProxyListSummaryView, {
all,
@@ -114,7 +116,7 @@ function ProxyGroupImpl({
);
}
-export const ProxyGroup = connect((s, { name, delay }) => {
+export const ProxyGroup = connect((s: State, { name, delay }) => {
const proxies = getProxies(s);
const collapsibleIsOpen = getCollapsibleIsOpen(s);
const proxySortBy = getProxySortBy(s);
@@ -133,3 +135,7 @@ export const ProxyGroup = connect((s, { name, delay }) => {
isOpen: collapsibleIsOpen[`proxyGroup:${name}`],
};
})(ProxyGroupImpl);
+function setIsTestingLatency (arg0: boolean) {
+ throw new Error('Function not implemented.');
+}
+
diff --git a/src/components/proxies/ProxyLatency.tsx b/src/components/proxies/ProxyLatency.tsx
index 48e55af..29036d5 100644
--- a/src/components/proxies/ProxyLatency.tsx
+++ b/src/components/proxies/ProxyLatency.tsx
@@ -3,14 +3,14 @@ import * as React from 'react';
import s0 from './ProxyLatency.module.scss';
type ProxyLatencyProps = {
- number: number;
+ number?: number;
color: string;
};
export function ProxyLatency({ number, color }: ProxyLatencyProps) {
return (
<span className={s0.proxyLatency} style={{ color }}>
- <span>{number} ms</span>
+ {typeof number === 'number' && number !== 0 ? number + ' ms' : ' '}
</span>
);
}
diff --git a/src/components/proxies/ProxyList.module.scss b/src/components/proxies/ProxyList.module.scss
index 1814929..12fea7e 100644
--- a/src/components/proxies/ProxyList.module.scss
+++ b/src/components/proxies/ProxyList.module.scss
@@ -6,8 +6,10 @@
}
.listSummaryView {
- margin: 8px 0;
+ margin: 14px 0;
display: grid;
grid-template-columns: repeat(auto-fill, 13px);
grid-gap: 10px;
+ place-items: center;
+ max-width: 900px;
}
diff --git a/src/components/proxies/ProxyList.tsx b/src/components/proxies/ProxyList.tsx
index a86bb88..3856c68 100644
--- a/src/components/proxies/ProxyList.tsx
+++ b/src/components/proxies/ProxyList.tsx
@@ -11,12 +11,7 @@ type ProxyListProps = {
show?: boolean;
};
-export function ProxyList({
- all,
- now,
- isSelectable,
- itemOnTapCallback,
-}: ProxyListProps) {
+export function ProxyList({ all, now, isSelectable, itemOnTapCallback }: ProxyListProps) {
const proxies = all;
return (
diff --git a/src/components/proxies/ProxyPageFab.tsx b/src/components/proxies/ProxyPageFab.tsx
index 7cc6d03..44e446f 100644
--- a/src/components/proxies/ProxyPageFab.tsx
+++ b/src/components/proxies/ProxyPageFab.tsx
@@ -2,12 +2,7 @@ import * as React from 'react';
import { Zap } from 'react-feather';
import { useTranslation } from 'react-i18next';
import { useUpdateProviderItems } from 'src/components/proxies/proxies.hooks';
-import {
- Action,
- Fab,
- IsFetching,
- position as fabPosition,
-} from 'src/components/shared/Fab';
+import { Action, Fab, IsFetching, position as fabPosition } from 'src/components/shared/Fab';
import { RotateIcon } from 'src/components/shared/RotateIcon';
import { requestDelayAll } from 'src/store/proxies';
import { DispatchFn, FormattedProxyProvider } from 'src/store/types';
diff --git a/src/components/proxies/ProxyProvider.module.scss b/src/components/proxies/ProxyProvider.module.scss
index 534305b..bc66bca 100644
--- a/src/components/proxies/ProxyProvider.module.scss
+++ b/src/components/proxies/ProxyProvider.module.scss
@@ -5,21 +5,25 @@
}
}
-.body {
+.main {
padding: 10px 15px;
@media (--breakpoint-not-small) {
padding: 10px 40px;
}
}
-.actionFooter {
+.head {
display: flex;
- button {
- margin: 0 5px;
- &:first-child {
- margin-left: 0;
- }
- }
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.action {
+ margin: 0 5px;
+ display: grid;
+ grid-template-columns: auto auto;
+ gap: 10px;
+ place-items: center;
}
.refresh {
diff --git a/src/components/proxies/ProxyProvider.tsx b/src/components/proxies/ProxyProvider.tsx
index 055a572..7939190 100644
--- a/src/components/proxies/ProxyProvider.tsx
+++ b/src/components/proxies/ProxyProvider.tsx
@@ -1,8 +1,8 @@
+import Tooltip from '@reach/tooltip';
import { formatDistance } from 'date-fns';
import * as React from 'react';
-import { RotateCw, Zap } from 'react-feather';
+import { RotateCw } from 'react-feather';
import Button from 'src/components/Button';
-import Collapsible from 'src/components/Collapsible';
import CollapsibleSectionHeader from 'src/components/CollapsibleSectionHeader';
import { useUpdateProviderItem } from 'src/components/proxies/proxies.hooks';
import { connect, useStoreActions } from 'src/components/StateProvider';
@@ -14,17 +14,20 @@ import {
getProxySortBy,
} from 'src/store/app';
import { getDelay, healthcheckProviderByName } from 'src/store/proxies';
-import { DelayMapping } from 'src/store/types';
+import { DelayMapping, State } from 'src/store/types';
+import { useState2 } from '$src/hooks/basic';
+
+import { ZapAnimated } from '../shared/ZapAnimated';
import { useFilteredAndSorted } from './hooks';
import { ProxyList, ProxyListSummaryView } from './ProxyList';
import s from './ProxyProvider.module.scss';
-const { useState, useCallback } = React;
+const { useCallback } = React;
type Props = {
name: string;
- proxies: Array<string>;
+ proxies: string[];
delay: DelayMapping;
hideUnavailableProxies: boolean;
proxySortBy: string;
@@ -48,21 +51,17 @@ function ProxyProviderImpl({
dispatch,
apiConfig,
}: Props) {
- const proxies = useFilteredAndSorted(
- all,
- delay,
- hideUnavailableProxies,
- proxySortBy
- );
- const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false);
+ const proxies = useFilteredAndSorted(all, delay, hideUnavailableProxies, proxySortBy);
+ const checkingHealth = useState2(false);
const updateProvider = useUpdateProviderItem({ dispatch, apiConfig, name });
- const healthcheckProvider = useCallback(async () => {
- setIsHealthcheckLoading(true);
- await dispatch(healthcheckProviderByName(apiConfig, name));
- setIsHealthcheckLoading(false);
- }, [apiConfig, dispatch, name, setIsHealthcheckLoading]);
+ const healthcheckProvider = useCallback(() => {
+ if (checkingHealth.value) return;
+ checkingHealth.set(true);
+ const stop = () => checkingHealth.set(false);
+ dispatch(healthcheckProviderByName(apiConfig, name)).then(stop, stop);
+ }, [apiConfig, dispatch, name, checkingHealth]);
const {
app: { updateCollapsibleIsOpen },
@@ -74,34 +73,33 @@ function ProxyProviderImpl({
const timeAgo = formatDistance(new Date(updatedAt), new Date());
return (
- <div className={s.body}>
- <CollapsibleSectionHeader
- name={name}
- toggle={toggle}
- type={vehicleType}
- isOpen={isOpen}
- qty={proxies.length}
- />
+ <div className={s.main}>
+ <div className={s.head}>
+ <CollapsibleSectionHeader
+ name={name}
+ toggle={toggle}
+ type={vehicleType}
+ isOpen={isOpen}
+ qty={proxies.length}
+ />
+
+ <div className={s.action}>
+ <Tooltip label={'Update'}>
+ <Button kind="circular" onClick={updateProvider}>
+ <Refresh />
+ </Button>
+ </Tooltip>
+ <Tooltip label={'Health Check'}>
+ <Button kind="circular" onClick={healthcheckProvider}>
+ <ZapAnimated animate={checkingHealth.value} size={16} />
+ </Button>
+ </Tooltip>
+ </div>
+ </div>
<div className={s.updatedAt}>
<small>Updated {timeAgo} ago</small>
</div>
- {/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element[]; isOpen: boolean; }' i... Remove this comment to see the full error message */}
- <Collapsible isOpen={isOpen}>
- <ProxyList all={proxies} />
- <div className={s.actionFooter}>
- <Button text="Update" start={<Refresh />} onClick={updateProvider} />
- <Button
- text="Health Check"
- start={<Zap size={16} />}
- onClick={healthcheckProvider}
- isLoading={isHealthcheckLoading}
- />
- </div>
- </Collapsible>
- {/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element; isOpen: boolean; }' is ... Remove this comment to see the full error message */}
- <Collapsible isOpen={!isOpen}>
- <ProxyListSummaryView all={proxies} />
- </Collapsible>
+ {isOpen ? <ProxyList all={proxies} /> : <ProxyListSummaryView all={proxies} />}
</div>
);
}
@@ -132,7 +130,7 @@ function Refresh() {
);
}
-const mapState = (s, { proxies, name }) => {
+const mapState = (s: State, { proxies, name }) => {
const hideUnavailableProxies = getHideUnavailableProxies(s);
const delay = getDelay(s);
const collapsibleIsOpen = getCollapsibleIsOpen(s);
diff --git a/src/components/proxies/ProxyProviderList.tsx b/src/components/proxies/ProxyProviderList.tsx
index 1528f37..754eeac 100644
--- a/src/components/proxies/ProxyProviderList.tsx
+++ b/src/components/proxies/ProxyProviderList.tsx
@@ -3,11 +3,7 @@ import ContentHeader from 'src/components/ContentHeader';
import { ProxyProvider } from 'src/components/proxies/ProxyProvider';
import { FormattedProxyProvider } from 'src/store/types';
-export function ProxyProviderList({
- items,
-}: {
- items: FormattedProxyProvider[];
-}) {
+export function ProxyProviderList({ items }: { items: FormattedProxyProvider[] }) {
if (items.length === 0) return null;
return (
diff --git a/src/components/proxies/Settings.tsx b/src/components/proxies/Settings.tsx
index 5e1ff98..55e18fe 100644
--- a/src/components/proxies/Settings.tsx
+++ b/src/components/proxies/Settings.tsx
@@ -2,11 +2,7 @@ import * as React from 'react';
import { useTranslation } from 'react-i18next';
import Select from 'src/components/shared/Select';
-import {
- getAutoCloseOldConns,
- getHideUnavailableProxies,
- getProxySortBy,
-} from '../../store/app';
+import { getAutoCloseOldConns, getHideUnavailableProxies, getProxySortBy } from '../../store/app';
import { connect, useStoreActions } from '../StateProvider';
import Switch from '../SwitchThemed';
import s from './Settings.module.scss';
diff --git a/src/components/proxies/hooks.tsx b/src/components/proxies/hooks.tsx
index 861c0e5..a43dc0e 100644
--- a/src/components/proxies/hooks.tsx
+++ b/src/components/proxies/hooks.tsx
@@ -45,22 +45,14 @@ const getSortDelay = (
const ProxySortingFns = {
Natural: (proxies: string[]) => proxies,
- LatencyAsc: (
- proxies: string[],
- delay: DelayMapping,
- proxyMapping?: ProxiesMapping
- ) => {
+ LatencyAsc: (proxies: string[], delay: DelayMapping, proxyMapping?: ProxiesMapping) => {
return proxies.sort((a, b) => {
const d1 = getSortDelay(delay[a], proxyMapping && proxyMapping[a]);
const d2 = getSortDelay(delay[b], proxyMapping && proxyMapping[b]);
return d1 - d2;
});
},
- LatencyDesc: (
- proxies: string[],
- delay: DelayMapping,
- proxyMapping?: ProxiesMapping
- ) => {
+ LatencyDesc: (proxies: string[], delay: DelayMapping, proxyMapping?: ProxiesMapping) => {
return proxies.sort((a, b) => {
const d1 = getSortDelay(delay[a], proxyMapping && proxyMapping[a]);
const d2 = getSortDelay(delay[b], proxyMapping && proxyMapping[b]);
diff --git a/src/components/proxies/proxies.hooks.tsx b/src/components/proxies/proxies.hooks.tsx
index ec51c9b..20695ac 100644
--- a/src/components/proxies/proxies.hooks.tsx
+++ b/src/components/proxies/proxies.hooks.tsx
@@ -14,11 +14,10 @@ export function useUpdateProviderItem({
apiConfig: ClashAPIConfig;
name: string;
}) {
- return useCallback(() => dispatch(updateProviderByName(apiConfig, name)), [
- apiConfig,
- dispatch,
- name,
- ]);
+ return useCallback(
+ () => dispatch(updateProviderByName(apiConfig, name)),
+ [apiConfig, dispatch, name]
+ );
}
export function useUpdateProviderItems({
diff --git a/src/components/rules/RuleProviderItem.module.scss b/src/components/rules/RuleProviderItem.module.scss
index 532ec8a..c3e1f07 100644
--- a/src/components/rules/RuleProviderItem.module.scss
+++ b/src/components/rules/RuleProviderItem.module.scss
@@ -13,6 +13,7 @@
.middle {
display: grid;
+ gap: 6px;
grid-template-rows: 1fr auto auto;
align-items: center;
}
@@ -21,13 +22,13 @@
color: #777;
}
-.refreshButtonWrapper {
+.action {
display: grid;
- place-items: center;
- opacity: 0;
- transition: opacity 0.2s;
+ gap: 4px;
+ grid-template-columns: auto 1fr;
+ align-items: center;
}
-.RuleProviderItem:hover .refreshButtonWrapper {
- opacity: 1;
+.refreshBtn {
+ padding: 5px;
}
diff --git a/src/components/rules/RuleProviderItem.tsx b/src/components/rules/RuleProviderItem.tsx
index fe4610e..c27e464 100644
--- a/src/components/rules/RuleProviderItem.tsx
+++ b/src/components/rules/RuleProviderItem.tsx
@@ -16,26 +16,22 @@ export function RuleProviderItem({
ruleCount,
apiConfig,
}) {
- const [onClickRefreshButton, isRefreshing] = useUpdateRuleProviderItem(
- name,
- apiConfig
- );
+ const [onClickRefreshButton, isRefreshing] = useUpdateRuleProviderItem(name, apiConfig);
const timeAgo = formatDistance(new Date(updatedAt), new Date());
return (
<div className={s.RuleProviderItem}>
<span className={s.left}>{idx}</span>
<div className={s.middle}>
<SectionNameType name={name} type={`${vehicleType} / ${behavior}`} />
- <div className={s.gray}>
- {ruleCount < 2 ? `${ruleCount} rule` : `${ruleCount} rules`}
+ <div className={s.gray}>{ruleCount < 2 ? `${ruleCount} rule` : `${ruleCount} rules`}</div>
+ <div className={s.action}>
+ <Button onClick={onClickRefreshButton} disabled={isRefreshing} className={s.refreshBtn}>
+ <RotateIcon isRotating={isRefreshing} size={13} />
+ <span className="visually-hidden">Refresh</span>
+ </Button>
+ <small className={s.gray}>Updated {timeAgo} ago</small>
</div>
- <small className={s.gray}>Updated {timeAgo} ago</small>
</div>
- <span className={s.refreshButtonWrapper}>
- <Button onClick={onClickRefreshButton} disabled={isRefreshing}>
- <RotateIcon isRotating={isRefreshing} />
- </Button>
- </span>
</div>
);
}
diff --git a/src/components/shared/Fab.tsx b/src/components/shared/Fab.tsx
index 832306e..8e72432 100644
--- a/src/components/shared/Fab.tsx
+++ b/src/components/shared/Fab.tsx
@@ -28,8 +28,7 @@ const AB: React.FC<ABProps> = ({ children, ...p }) => (
</button>
);
-interface MBProps
- extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'tabIndex'> {
+interface MBProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'tabIndex'> {
tabIndex?: number;
}
@@ -77,10 +76,7 @@ const Fab: React.FC<FabProps> = ({
return event === 'click' ? (isOpen ? close() : open()) : null;
};
- const actionOnClick = (
- e: React.FormEvent,
- userFunc: (e: React.FormEvent) => void
- ) => {
+ const actionOnClick = (e: React.FormEvent, userFunc: (e: React.FormEvent) => void) => {
e.persist();
setIsOpen(false);
setTimeout(() => {
@@ -141,9 +137,7 @@ const Fab: React.FC<FabProps> = ({
</MB>
{text && (
<span
- className={`${'right' in style ? 'right' : ''} ${
- alwaysShowTitle ? 'always-show' : ''
- }`}
+ className={`${'right' in style ? 'right' : ''} ${alwaysShowTitle ? 'always-show' : ''}`}
aria-hidden={ariaHidden}
>
{text}
diff --git a/src/components/shared/RotateIcon.tsx b/src/components/shared/RotateIcon.tsx
index 7e3ceae..d291ece 100644
--- a/src/components/shared/RotateIcon.tsx
+++ b/src/components/shared/RotateIcon.tsx
@@ -4,13 +4,12 @@ import { RotateCw } from 'react-feather';
import s from './RotateIcon.module.scss';
-export function RotateIcon({ isRotating }: { isRotating: boolean }) {
- const cls = cx(s.rotate, {
- [s.isRotating]: isRotating,
- });
+export function RotateIcon(props: { isRotating: boolean; size?: number }) {
+ const size = props.size || 16;
+ const cls = cx(s.rotate, { [s.isRotating]: props.isRotating });
return (
<span className={cls}>
- <RotateCw width={16} />
+ <RotateCw size={size} />
</span>
);
}
diff --git a/src/components/shared/TextFitler.tsx b/src/components/shared/TextFitler.tsx
index e4a4a88..7af61ac 100644
--- a/src/components/shared/TextFitler.tsx
+++ b/src/components/shared/TextFitler.tsx
@@ -4,10 +4,7 @@ import { useTextInut } from 'src/hooks/useTextInput';
import s from './TextFitler.module.scss';
-export function TextFilter(props: {
- textAtom: RecoilState<string>;
- placeholder?: string;
-}) {
+export function TextFilter(props: { textAtom: RecoilState<string>; placeholder?: string }) {
const [onChange, text] = useTextInut(props.textAtom);
return (
<input
diff --git a/src/components/shared/ThemeSwitcher.module.scss b/src/components/shared/ThemeSwitcher.module.scss
index c5de126..951376a 100644
--- a/src/components/shared/ThemeSwitcher.module.scss
+++ b/src/components/shared/ThemeSwitcher.module.scss
@@ -29,7 +29,8 @@
height: var(--sz);
select {
cursor: pointer;
- padding-left: var(--sz);
+ padding-left: calc(var(--sz) - 2px);
+ font-size: 0;
width: var(--sz);
height: var(--sz);
appearance: none;
diff --git a/src/components/shared/ZapAnimated.module.scss b/src/components/shared/ZapAnimated.module.scss
new file mode 100644
index 0000000..e4cb37b
--- /dev/null
+++ b/src/components/shared/ZapAnimated.module.scss
@@ -0,0 +1,12 @@
+.animate {
+ --saturation: 70%;
+ stroke: hsl(46deg var(--saturation) 45%);
+ animation: zap-pulse 0.7s 0s ease-in-out none normal infinite;
+}
+
+// prettier-ignore
+@keyframes zap-pulse {
+ 0% { stroke: hsl(46deg var(--saturation) 45%); }
+ 50% { stroke: hsl(46deg var(--saturation) 95%); }
+ 100% { stroke: hsl(46deg var(--saturation) 45%); }
+}
diff --git a/src/components/shared/ZapAnimated.tsx b/src/components/shared/ZapAnimated.tsx
new file mode 100644
index 0000000..e3b153a
--- /dev/null
+++ b/src/components/shared/ZapAnimated.tsx
@@ -0,0 +1,25 @@
+import cx from 'clsx';
+import * as React from 'react';
+
+import s from './ZapAnimated.module.scss';
+
+export function ZapAnimated(props: { size?: number; animate?: boolean }) {
+ const size = props.size || 24;
+ const cls = cx({ [s.animate]: props.animate });
+ return (
+ <svg
+ className={cls}
+ xmlns="http://www.w3.org/2000/svg"
+ width={size}
+ height={size}
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
+ </svg>
+ );
+}
diff --git a/src/components/svg/Equalizer.tsx b/src/components/svg/Equalizer.tsx
index 274720f..ae3c858 100644
--- a/src/components/svg/Equalizer.tsx
+++ b/src/components/svg/Equalizer.tsx
@@ -5,10 +5,7 @@ type Props = {
color?: string;
};
-export default function Equalizer({
- color = 'currentColor',
- size = 24,
-}: Props) {
+export default function Equalizer({ color = 'currentColor', size = 24 }: Props) {
return (
<svg
fill="none"