diff options
| author | Haishan <[email protected]> | 2020-04-26 17:35:03 +0800 |
|---|---|---|
| committer | Haishan <[email protected]> | 2020-04-26 17:59:02 +0800 |
| commit | 94e2b1e3985f8f4cfeb26a43c59cada184c7d4aa (patch) | |
| tree | 2d88e5e3ace986e1c08f3eca5e787d1339249dd2 /src/components | |
| parent | 7cdbba5bf47062f80a0dc7d80a62ff977d4f568e (diff) | |
feat: allow change proxies sorting in group
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/Modal.module.css | 2 | ||||
| -rw-r--r-- | src/components/ModalCloseAllConnections.module.css | 2 | ||||
| -rw-r--r-- | src/components/Proxies.js | 62 | ||||
| -rw-r--r-- | src/components/Proxies.module.css | 10 | ||||
| -rw-r--r-- | src/components/ProxyGroup.js | 90 | ||||
| -rw-r--r-- | src/components/ProxyProvider.js | 41 | ||||
| -rw-r--r-- | src/components/Root.css | 20 | ||||
| -rw-r--r-- | src/components/proxies/Settings.js | 75 | ||||
| -rw-r--r-- | src/components/proxies/Settings.module.css | 17 | ||||
| -rw-r--r-- | src/components/rtf.css | 9 | ||||
| -rw-r--r-- | src/components/shared/BaseModal.js | 30 | ||||
| -rw-r--r-- | src/components/shared/BaseModal.module.css | 17 | ||||
| -rw-r--r-- | src/components/shared/Select.module.css | 29 | ||||
| -rw-r--r-- | src/components/shared/Select.tsx | 21 | ||||
| -rw-r--r-- | src/components/svg/Equalizer.tsx | 30 |
15 files changed, 366 insertions, 89 deletions
diff --git a/src/components/Modal.module.css b/src/components/Modal.module.css index 1b183bc..6192a1f 100644 --- a/src/components/Modal.module.css +++ b/src/components/Modal.module.css @@ -10,7 +10,7 @@ .content { outline: none; - position: absolute; + position: relative; color: #ddd; top: 50%; left: 50%; diff --git a/src/components/ModalCloseAllConnections.module.css b/src/components/ModalCloseAllConnections.module.css index f3b54c1..9bb7c6a 100644 --- a/src/components/ModalCloseAllConnections.module.css +++ b/src/components/ModalCloseAllConnections.module.css @@ -6,7 +6,7 @@ color: var(--color-text); max-width: 300px; line-height: 1.4; - transform: translate(-50%, -50%) scale(1.5); + transform: translate(-50%, -50%) scale(1.2); opacity: 0.6; transition: all 0.3s ease; } diff --git a/src/components/Proxies.js b/src/components/Proxies.js index acb26dd..729b57a 100644 --- a/src/components/Proxies.js +++ b/src/components/Proxies.js @@ -1,39 +1,34 @@ import React from 'react'; -import { connect, useStoreActions } from './StateProvider'; +import { connect } from './StateProvider'; +import Button from './Button'; import ContentHeader from './ContentHeader'; import ProxyGroup from './ProxyGroup'; -import { Zap, Filter, Circle } from 'react-feather'; +import BaseModal from './shared/BaseModal'; +import Settings from './proxies/Settings'; +import Equalizer from './svg/Equalizer'; +import { Zap } from 'react-feather'; import ProxyProviderList from './ProxyProviderList'; -import { Fab, Action } from 'react-tiny-fab'; +import { Fab } from 'react-tiny-fab'; import './rtf.css'; import s0 from './Proxies.module.css'; import { getDelay, - getRtFilterSwitch, getProxyGroupNames, getProxyProviders, fetchProxies, - requestDelayAll + requestDelayAll, } from '../store/proxies'; import { getClashAPIConfig } from '../store/app'; -const { useEffect, useCallback, useRef } = React; +const { useState, useEffect, useCallback, useRef } = React; -function Proxies({ - dispatch, - groupNames, - delay, - proxyProviders, - apiConfig, - filterZeroRT -}) { +function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) { const refFetchedTimestamp = useRef({}); - const { toggleUnavailableProxiesFilter } = useStoreActions(); const requestDelayAllFn = useCallback( () => dispatch(requestDelayAll(apiConfig)), [apiConfig, dispatch] @@ -62,11 +57,27 @@ function Proxies({ return () => window.removeEventListener('focus', fn, false); }, [fetchProxiesHooked]); + const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); + const closeSettingsModal = useCallback(() => { + setIsSettingsModalOpen(false); + }, []); + return ( <> + <div className={s0.topBar}> + <Button kind="minimal" onClick={() => setIsSettingsModalOpen(true)}> + <Equalizer size={16} /> + </Button> + </div> + <BaseModal + isOpen={isSettingsModalOpen} + onRequestClose={closeSettingsModal} + > + <Settings /> + </BaseModal> <ContentHeader title="Proxies" /> <div> - {groupNames.map(groupName => { + {groupNames.map((groupName) => { return ( <div className={s0.group} key={groupName}> <ProxyGroup @@ -81,27 +92,20 @@ function Proxies({ </div> <ProxyProviderList items={proxyProviders} /> <div style={{ height: 60 }} /> - <Fab icon={<Circle />}> - <Action text="Test Latency" onClick={requestDelayAllFn}> - <Zap width={16} /> - </Action> - <Action - text={(filterZeroRT ? 'Show' : 'Hide') + ' Unavailable Proxies'} - onClick={toggleUnavailableProxiesFilter} - > - <Filter width={16} /> - </Action> - </Fab> + <Fab + icon={<Zap width={16} />} + onClick={requestDelayAllFn} + text="Test Latency" + ></Fab> </> ); } -const mapState = s => ({ +const mapState = (s) => ({ apiConfig: getClashAPIConfig(s), groupNames: getProxyGroupNames(s), proxyProviders: getProxyProviders(s), delay: getDelay(s), - filterZeroRT: getRtFilterSwitch(s) }); export default connect(mapState)(Proxies); diff --git a/src/components/Proxies.module.css b/src/components/Proxies.module.css index 5520a2e..2a72f51 100644 --- a/src/components/Proxies.module.css +++ b/src/components/Proxies.module.css @@ -1,3 +1,13 @@ +.topBar { + position: sticky; + top: 0; + z-index: 1; + background: var(--color-background); + display: flex; + justify-content: flex-end; + padding: 5px 5px 2px 0; +} + .group { padding: 10px 15px; @media (--breakpoint-not-small) { diff --git a/src/components/ProxyGroup.js b/src/components/ProxyGroup.js index d7cf203..b1a53c0 100644 --- a/src/components/ProxyGroup.js +++ b/src/components/ProxyGroup.js @@ -3,8 +3,12 @@ import cx from 'classnames'; import memoizeOne from 'memoize-one'; import { connect, useStoreActions } from './StateProvider'; -import { getProxies, getRtFilterSwitch } from '../store/proxies'; -import { getCollapsibleIsOpen } from '../store/app'; +import { getProxies } from '../store/proxies'; +import { + getCollapsibleIsOpen, + getProxySortBy, + getHideUnavailableProxies, +} from '../store/app'; import CollapsibleSectionHeader from './CollapsibleSectionHeader'; import Proxy, { ProxySmall } from './Proxy'; @@ -18,7 +22,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) { const isSelectable = useMemo(() => type === 'Selector', [type]); const { - app: { updateCollapsibleIsOpen } + app: { updateCollapsibleIsOpen }, } = useStoreActions(); const toggle = useCallback(() => { @@ -26,7 +30,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) { }, [isOpen, updateCollapsibleIsOpen, name]); const itemOnTapCallback = useCallback( - proxyName => { + (proxyName) => { if (!isSelectable) return; dispatch(switchProxy(apiConfig, name, proxyName)); }, @@ -60,23 +64,23 @@ type ProxyListProps = { all: string[], now?: string, isSelectable?: boolean, - itemOnTapCallback?: string => void, - show?: boolean + itemOnTapCallback?: (string) => void, + show?: boolean, }; export function ProxyList({ all, now, isSelectable, itemOnTapCallback, - sortedAll + sortedAll, }: ProxyListProps) { const proxies = sortedAll || all; return ( <div className={s0.list}> - {proxies.map(proxyName => { + {proxies.map((proxyName) => { const proxyClassName = cx(s0.proxy, { - [s0.proxySelectable]: isSelectable + [s0.proxySelectable]: isSelectable, }); return ( <div @@ -107,7 +111,7 @@ const getSortDelay = (d, w) => { }; function filterAvailableProxies(list, delay) { - return list.filter(name => { + return list.filter((name) => { const d = delay[name]; if (d === undefined) { return true; @@ -120,19 +124,50 @@ function filterAvailableProxies(list, delay) { }); } -function filterAvailableProxiesAndSortImpl(all, delay, filterByRt) { +const ProxySortingFns = { + Natural: (proxies, _delay) => { + return proxies; + }, + LatencyAsc: (proxies, delay) => { + return proxies.sort((a, b) => { + const d1 = getSortDelay(delay[a], 999999); + const d2 = getSortDelay(delay[b], 999999); + return d1 - d2; + }); + }, + LatencyDesc: (proxies, delay) => { + return proxies.sort((a, b) => { + const d1 = getSortDelay(delay[a], 999999); + const d2 = getSortDelay(delay[b], 999999); + return d2 - d1; + }); + }, + NameAsc: (proxies) => { + return proxies.sort(); + }, + NameDesc: (proxies) => { + return proxies.sort((a, b) => { + if (a > b) return -1; + if (a < b) return 1; + return 0; + }); + }, +}; + +function filterAvailableProxiesAndSortImpl( + all, + delay, + hideUnavailableProxies, + proxySortBy +) { // all is freezed let filtered = [...all]; - if (filterByRt) { + if (hideUnavailableProxies) { filtered = filterAvailableProxies(all, delay); } - - return filtered.sort((first, second) => { - const d1 = getSortDelay(delay[first], 999999); - const d2 = getSortDelay(delay[second], 999999); - return d1 - d2; - }); + return ProxySortingFns[proxySortBy](filtered, delay); } + export const filterAvailableProxiesAndSort = memoizeOne( filterAvailableProxiesAndSortImpl ); @@ -141,13 +176,13 @@ export function ProxyListSummaryView({ all, now, isSelectable, - itemOnTapCallback + itemOnTapCallback, }: ProxyListProps) { return ( <div className={s0.list}> - {all.map(proxyName => { + {all.map((proxyName) => { const proxyClassName = cx(s0.proxy, { - [s0.proxySelectable]: isSelectable + [s0.proxySelectable]: isSelectable, }); return ( <div @@ -168,14 +203,21 @@ export function ProxyListSummaryView({ export default connect((s, { name, delay }) => { const proxies = getProxies(s); - const filterByRt = getRtFilterSwitch(s); const collapsibleIsOpen = getCollapsibleIsOpen(s); + const proxySortBy = getProxySortBy(s); + const hideUnavailableProxies = getHideUnavailableProxies(s); + const group = proxies[name]; const { all, type, now } = group; return { - all: filterAvailableProxiesAndSort(all, delay, filterByRt), + all: filterAvailableProxiesAndSort( + all, + delay, + hideUnavailableProxies, + proxySortBy + ), type, now, - isOpen: collapsibleIsOpen[`proxyGroup:${name}`] + isOpen: collapsibleIsOpen[`proxyGroup:${name}`], }; })(ProxyGroup); diff --git a/src/components/ProxyProvider.js b/src/components/ProxyProvider.js index 9486128..64dc93b 100644 --- a/src/components/ProxyProvider.js +++ b/src/components/ProxyProvider.js @@ -9,16 +9,20 @@ import CollapsibleSectionHeader from './CollapsibleSectionHeader'; import { ProxyList, ProxyListSummaryView, - filterAvailableProxiesAndSort + filterAvailableProxiesAndSort, } from './ProxyGroup'; import Button from './Button'; -import { getClashAPIConfig, getCollapsibleIsOpen } from '../store/app'; +import { + getClashAPIConfig, + getCollapsibleIsOpen, + getProxySortBy, + getHideUnavailableProxies, +} from '../store/app'; import { getDelay, - getRtFilterSwitch, updateProviderByName, - healthcheckProviderByName + healthcheckProviderByName, } from '../store/proxies'; import s from './ProxyProvider.module.css'; @@ -31,8 +35,8 @@ type Props = { type: 'Proxy' | 'Rule', vehicleType: 'HTTP' | 'File' | 'Compatible', updatedAt?: string, - dispatch: any => void, - isOpen: boolean + dispatch: (any) => void, + isOpen: boolean, }; function ProxyProvider({ @@ -42,7 +46,7 @@ function ProxyProvider({ updatedAt, isOpen, dispatch, - apiConfig + apiConfig, }: Props) { const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false); const updateProvider = useCallback( @@ -56,7 +60,7 @@ function ProxyProvider({ }, [apiConfig, dispatch, name, setIsHealthcheckLoading]); const { - app: { updateCollapsibleIsOpen } + app: { updateCollapsibleIsOpen }, } = useStoreActions(); // const [isCollapsibleOpen, setCollapsibleOpen] = useState(false); @@ -101,11 +105,11 @@ function ProxyProvider({ const button = { rest: { scale: 1 }, // hover: { scale: 1.1 }, - pressed: { scale: 0.95 } + pressed: { scale: 0.95 }, }; const arrow = { rest: { rotate: 0 }, - hover: { rotate: 360, transition: { duration: 0.3 } } + hover: { rotate: 360, transition: { duration: 0.3 } }, }; function Refresh() { return ( @@ -124,18 +128,23 @@ function Refresh() { } const mapState = (s, { proxies, name }) => { - const filterByRt = getRtFilterSwitch(s); + const hideUnavailableProxies = getHideUnavailableProxies(s); const delay = getDelay(s); const collapsibleIsOpen = getCollapsibleIsOpen(s); const apiConfig = getClashAPIConfig(s); + + const proxySortBy = getProxySortBy(s); + return { apiConfig, - proxies: filterAvailableProxiesAndSort(proxies, delay, filterByRt), - isOpen: collapsibleIsOpen[`proxyProvider:${name}`] + proxies: filterAvailableProxiesAndSort( + proxies, + delay, + hideUnavailableProxies, + proxySortBy + ), + isOpen: collapsibleIsOpen[`proxyProvider:${name}`], }; }; -// const mapState = s => ({ -// apiConfig: getClashAPIConfig(s) -// }); export default connect(mapState)(ProxyProvider); diff --git a/src/components/Root.css b/src/components/Root.css index fd7d6d9..22cae24 100644 --- a/src/components/Root.css +++ b/src/components/Root.css @@ -1,15 +1,3 @@ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 400; - src: local('Roboto Mono'), local('RobotoMono-Regular'), - url('https://cdn.jsdelivr.net/npm/@hsjs/[email protected]/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, - U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, - U+FEFF, U+FFFD; -} - .relative { position: relative; } @@ -88,6 +76,7 @@ body.dark { --color-background: #202020; --color-text: #ddd; --color-text-secondary: #ccc; + --color-text-highlight: #fff; --color-bg-sidebar: #2d2d30; --color-sb-active-row-bg: #494b4e; --color-input-bg: #2d2d30; @@ -95,18 +84,22 @@ body.dark { --color-toggle-bg: #353535; --color-toggle-selected: #181818; --color-icon: #c7c7c7; + --color-separator: #333; --color-btn-bg: #232323; --color-btn-fg: #bebebe; --color-bg-proxy: #303030; --color-row-odd: #282828; --bg-modal: #1f1f20; --bg-near-transparent: rgba(255, 255, 255, 0.1); + --select-border-color: #040404; + --select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20); } body.light { --color-background: #fbfbfb; --color-text: #222; --color-text-secondary: #646464; + --color-text-highlight: #040404; --color-bg-sidebar: #e7e7e7; --color-sb-active-row-bg: #d0d0d0; --color-input-bg: #ffffff; @@ -114,12 +107,15 @@ body.light { --color-toggle-bg: #ffffff; --color-toggle-selected: #d7d7d7; --color-icon: #5b5b5b; + --color-separator: #ccc; --color-btn-bg: #f4f4f4; --color-btn-fg: #101010; --color-bg-proxy: #e7e7e7; --color-row-odd: #f5f5f5; --bg-modal: #fbfbfb; --bg-near-transparent: rgba(0, 0, 0, 0.1); + --select-border-color: #999999; + --select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20); } .flexCenter { diff --git a/src/components/proxies/Settings.js b/src/components/proxies/Settings.js new file mode 100644 index 0000000..e21ae72 --- /dev/null +++ b/src/components/proxies/Settings.js @@ -0,0 +1,75 @@ +import * as React from 'react'; + +import { getProxySortBy, getHideUnavailableProxies } from '../../store/app'; + +import Switch from '../SwitchThemed'; +import { connect, useStoreActions } from '../StateProvider'; +import Select from '../shared/Select'; +import s from './Settings.module.css'; + +const options = [ + ['Natural', 'Original order in config file'], + ['LatencyAsc', 'By latency from small to big'], + ['LatencyDesc', 'By latency from big to small'], + ['NameAsc', 'By name alphabetically (A-Z)'], + ['NameDesc', 'By name alphabetically (Z-A)'], +]; + +const { useCallback } = React; + +function Settings({ appConfig }) { + const { + app: { updateAppConfig }, + } = useStoreActions(); + + const handleProxySortByOnChange = useCallback( + (e) => { + updateAppConfig('proxySortBy', e.target.value); + }, + [updateAppConfig] + ); + + const handleHideUnavailablesSwitchOnChange = useCallback( + (v) => { + updateAppConfig('hideUnavailableProxies', v); + }, + [updateAppConfig] + ); + return ( + <> + <div className={s.labeledInput}> + <span>Sorting in group</span> + <div> + <Select + options={options} + selected={appConfig.proxySortBy} + onChange={handleProxySortByOnChange} + /> + </div> + </div> + <hr /> + <div className={s.labeledInput}> + <span>Hide unavailable proxies</span> + <div> + <Switch + name="hideUnavailableProxies" + checked={appConfig.hideUnavailableProxies} + onChange={handleHideUnavailablesSwitchOnChange} + /> + </div> + </div> + </> + ); +} + +const mapState = (s) => { + const proxySortBy = getProxySortBy(s); + const hideUnavailableProxies = getHideUnavailableProxies(s); + return { + appConfig: { + proxySortBy, + hideUnavailableProxies, + }, + }; +}; +export default connect(mapState)(Settings); diff --git a/src/components/proxies/Settings.module.css b/src/components/proxies/Settings.module.css new file mode 100644 index 0000000..364d07d --- /dev/null +++ b/src/components/proxies/Settings.module.css @@ -0,0 +1,17 @@ +.labeledInput { + max-width: 85vw; + width: 400px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; + padding: 13px 0; +} + +hr { + height: 1px; + background-color: var(--color-separator); + border: none; + outline: none; + margin: 1rem 0px; +} diff --git a/src/components/rtf.css b/src/components/rtf.css index 1a68f6b..a61b35d 100644 --- a/src/components/rtf.css +++ b/src/components/rtf.css @@ -13,7 +13,7 @@ } .rtf.open .rtf--mb > * { transform-origin: center center; - transform: rotate(315deg); + transform: rotate(360deg); transition: ease-in-out transform 0.2s; } .rtf.open .rtf--mb > ul { @@ -107,18 +107,15 @@ } .rtf--mb { - height: 56px; - width: 56px; + height: 48px; + width: 48px; z-index: 9999; - /* background-color: #666666; */ background: #387cec; - /* background: var(--color-btn-bg); */ display: inline-flex; justify-content: center; align-items: center; position: relative; border: none; - /* border: 1px solid #555; */ border-radius: 50%; box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28); cursor: pointer; diff --git a/src/components/shared/BaseModal.js b/src/components/shared/BaseModal.js new file mode 100644 index 0000000..8bbdbf4 --- /dev/null +++ b/src/components/shared/BaseModal.js @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import Modal from 'react-modal'; +import cx from 'classnames'; + +import modalStyle from '../Modal.module.css'; +import s from './BaseModal.module.css'; + +const { useMemo } = React; + +export default function BaseModal({ isOpen, onRequestClose, children }) { + const className = useMemo( + () => ({ + base: cx(modalStyle.content, s.cnt), + afterOpen: s.afterOpen, + beforeClose: '', + }), + [] + ); + return ( + <Modal + isOpen={isOpen} + onRequestClose={onRequestClose} + className={className} + overlayClassName={cx(modalStyle.overlay, s.overlay)} + > + {children} + </Modal> + ); +} diff --git a/src/components/shared/BaseModal.module.css b/src/components/shared/BaseModal.module.css new file mode 100644 index 0000000..1d206e1 --- /dev/null +++ b/src/components/shared/BaseModal.module.css @@ -0,0 +1,17 @@ +.overlay { + background-color: rgba(0, 0, 0, 0.6); +} +.cnt { + position: absolute; + background-color: var(--bg-modal); + color: var(--color-text); + line-height: 1.4; + opacity: 0.6; + transition: all 0.3s ease; + transform: translate(-50%, -50%) scale(1.2); + box-shadow: rgba(0, 0, 0, 0.12) 0px 4px 4px, rgba(0, 0, 0, 0.24) 0px 16px 32px; +} +.afterOpen { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} diff --git a/src/components/shared/Select.module.css b/src/components/shared/Select.module.css new file mode 100644 index 0000000..32343ea --- /dev/null +++ b/src/components/shared/Select.module.css @@ -0,0 +1,29 @@ +.select { + height: 30px; + width: 100%; + padding-left: 8px; + background-color: transparent; + appearance: none; + /* background-color: rgb(36, 36, 36); */ + /* -webkit-appearance: none; */ + color: var(--color-text); + /* color: rgb(153, 153, 153); */ + padding-right: 20px; + background-image: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20); + border-radius: 4px; + border-width: 1px; + border-style: solid; + border-image: initial; + border-color: var(--select-border-color); + transition: all 100ms ease 0s; + background-position: calc(100% - 8px) center; + background-repeat: no-repeat; +} + +.select:hover, +.select:focus { + border-color: rgb(52, 52, 52); + outline: none !important; + color: var(--color-text-highlight); + background-image: var(--select-bg-hover); +} diff --git a/src/components/shared/Select.tsx b/src/components/shared/Select.tsx new file mode 100644 index 0000000..03ac084 --- /dev/null +++ b/src/components/shared/Select.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +import s from './Select.module.css'; + +type Props = { + options: Array<string[]>; + selected: string; + onChange: (event: React.ChangeEvent<HTMLSelectElement>) => any; +}; + +export default function Select({ options, selected, onChange }: Props) { + return ( + <select className={s.select} value={selected} onChange={onChange}> + {options.map(([value, name]) => ( + <option key={value} value={value}> + {name} + </option> + ))} + </select> + ); +} diff --git a/src/components/svg/Equalizer.tsx b/src/components/svg/Equalizer.tsx new file mode 100644 index 0000000..274720f --- /dev/null +++ b/src/components/svg/Equalizer.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +type Props = { + size?: number; + color?: string; +}; + +export default function Equalizer({ + color = 'currentColor', + size = 24, +}: Props) { + return ( + <svg + fill="none" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + width={size} + height={size} + stroke={color} + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <path d="M2 6h9M18.5 6H22" /> + <circle cx="16" cy="6" r="2" /> + <path d="M22 18h-9M6 18H2" /> + <circle r="2" transform="matrix(-1 0 0 1 8 18)" /> + </svg> + ); +} |
