summaryrefslogtreecommitdiff
path: root/src/components/backend
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/backend')
-rw-r--r--src/components/backend/APIConfig.module.scss266
-rw-r--r--src/components/backend/APIConfig.tsx210
-rw-r--r--src/components/backend/APIDiscovery.module.scss5
-rw-r--r--src/components/backend/APIDiscovery.tsx2
-rw-r--r--src/components/backend/BackendList.module.scss186
-rw-r--r--src/components/backend/BackendList.tsx152
6 files changed, 608 insertions, 213 deletions
diff --git a/src/components/backend/APIConfig.module.scss b/src/components/backend/APIConfig.module.scss
index 6581d46..cfc740f 100644
--- a/src/components/backend/APIConfig.module.scss
+++ b/src/components/backend/APIConfig.module.scss
@@ -1,52 +1,268 @@
.root {
+ // 弹层里这块是 flex item,写死宽度才不会被内容撑成 shrink-to-fit
+ width: 520px;
+ max-width: 100%;
+ margin: 0 auto;
+ padding: 40px 0 48px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
&:focus {
outline: none;
}
}
-.header {
+/* 顶部品牌区 */
+.hero {
display: flex;
- justify-content: center;
+ flex-direction: column;
align-items: center;
+ text-align: center;
+ gap: 8px;
+}
- .icon {
- --stroke: #f3f3f3;
- color: #20497e;
- opacity: 0.7;
- transition: opacity 400ms;
- &:hover {
- opacity: 1;
- }
+.logo {
+ --stroke: var(--color-background);
+ color: #20497e;
+ opacity: 0.85;
+ transition: opacity 400ms;
+ &:hover {
+ opacity: 1;
}
}
-.body {
- padding: 15px 0 0;
+.title {
+ margin: 4px 0 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--color-text-highlight);
+}
+
+.desc {
+ margin: 0;
+ font-size: 0.85rem;
+ line-height: 1.5;
+ color: var(--color-text-secondary);
+}
+
+/* 表单卡片 */
+.card {
+ padding: 20px;
+ border-radius: var(--border-radius);
+ background: var(--color-card);
+ border: 1px solid var(--color-card-border);
+ box-shadow: var(--shadow-card);
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+/* 协议 / 主机 / 端口一行,secret 独占一行 */
+.grid {
+ display: grid;
+ gap: 14px 12px;
+ grid-template-columns: minmax(140px, auto) minmax(0, 1fr) 88px;
+ grid-template-areas:
+ 'protocol host port'
+ 'secret secret secret';
+
+ @media screen and (max-width: 30em) {
+ grid-template-columns: minmax(0, 1fr) 88px;
+ grid-template-areas:
+ 'protocol protocol'
+ 'host port'
+ 'secret secret';
+ }
+}
+
+.protocol {
+ grid-area: protocol;
+}
+.host {
+ grid-area: host;
+}
+.port {
+ grid-area: port;
+}
+.secret {
+ grid-area: secret;
+}
+
+.field {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
}
-.hostnamePort {
+.label {
display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+}
+
+.optional {
+ padding: 1px 6px;
+ border-radius: 100px;
+ background: var(--color-badge-bg);
+ color: var(--color-badge-fg);
+ font-size: 0.65rem;
+ font-weight: 400;
+ text-transform: lowercase;
+}
+
+.input {
+ -webkit-appearance: none;
+ width: 100%;
+ min-width: 0;
+ height: 38px;
+ box-sizing: border-box;
+ padding: 0 10px;
+ font-family: inherit;
+ font-size: 0.875rem;
+ color: var(--color-text);
+ background: var(--color-input-bg);
+ border: 1px solid var(--color-input-border);
+ border-radius: 10px;
+ outline: none;
+ transition:
+ border-color 0.2s ease,
+ box-shadow 0.2s ease;
- div {
- flex: 1 1 auto;
+ &::placeholder {
+ color: var(--color-text-secondary);
+ opacity: 0.6;
}
- div:nth-child(2) {
- flex-grow: 0;
- flex-basis: 120px;
- margin-left: 10px;
+ &:focus {
+ border-color: var(--color-focus-blue);
+ box-shadow: 0 0 0 3px var(--color-accent-soft-bg);
}
}
+/* 协议分段器与输入框对齐高度 */
+.protocolControl {
+ height: 38px;
+}
+
+.inputWithAction {
+ position: relative;
+ display: flex;
+ align-items: center;
+
+ .input {
+ padding-right: 40px;
+ }
+}
+
+.iconBtn {
+ position: absolute;
+ right: 4px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ appearance: none;
+ border: none;
+ border-radius: 8px;
+ background: transparent;
+ color: var(--color-icon);
+ cursor: pointer;
+
+ &:hover {
+ color: var(--color-text);
+ background: var(--color-hover-soft);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--color-focus-blue);
+ outline-offset: -2px;
+ }
+}
+
+.actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.status {
+ min-width: 0;
+ font-size: 0.75rem;
+ line-height: 1.4;
+}
+
+.buttons {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.preview {
+ display: block;
+ color: var(--color-text-secondary);
+ font-family: var(--font-mono);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.error {
- height: 20px;
- font-size: 0.8em;
- color: #ff8b8b;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--color-danger);
+
+ svg {
+ flex-shrink: 0;
+ }
+}
+
+/* 已保存后端 */
+.saved {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
}
-.footer {
- padding: 5px 0 10px;
+.savedHeader {
display: flex;
- justify-content: flex-end;
align-items: center;
+ gap: 8px;
+ padding: 0 4px;
+}
+
+.savedTitle {
+ margin: 0;
+ font-size: 0.8rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+ color: var(--color-text-secondary);
+}
+
+.count {
+ padding: 1px 8px;
+ border-radius: 100px;
+ background: var(--color-badge-bg);
+ color: var(--color-badge-fg);
+ font-size: 0.7rem;
+}
+
+.empty {
+ margin: 0;
+ padding: 20px;
+ text-align: center;
+ font-size: 0.8rem;
+ color: var(--color-text-secondary);
+ border: 1px dashed var(--color-card-border);
+ border-radius: var(--border-radius);
}
diff --git a/src/components/backend/APIConfig.tsx b/src/components/backend/APIConfig.tsx
index 72c72ca..3f26690 100644
--- a/src/components/backend/APIConfig.tsx
+++ b/src/components/backend/APIConfig.tsx
@@ -1,13 +1,18 @@
+import cx from 'clsx';
import * as React from 'react';
+import { useTranslation } from 'react-i18next';
-import Button from '~/components/Button';
-import Field from '~/components/Field';
-import SvgYacd from '~/components/SvgYacd';
+import Button from '~/components/shared/Button';
+import { AlertCircle, Eye, EyeOff } from '~/components/shared/FeatherIcons';
+import { SegmentedControl } from '~/components/shared/SegmentedControl';
+import SvgYacd from '~/components/shared/SvgYacd';
+import { useToggle } from '~/hooks/basic';
import { useBackendConfigForm } from '~/modules/backend/hooks';
+import type { Protocol } from '~/modules/backend/utils';
import type { ClashAPIConfigWithAddedAt } from '~/store/types';
import type { ClashAPIConfig } from '~/types';
-import s0 from './APIConfig.module.scss';
+import s from './APIConfig.module.scss';
import { BackendList } from './BackendList';
type Props = {
@@ -16,9 +21,13 @@ type Props = {
onAddConfig: (config: ClashAPIConfig) => void;
onRemoveConfig: (config: ClashAPIConfig) => void;
onSelectConfig: (config: ClashAPIConfig) => void;
+ onUpdateConfig: (prev: ClashAPIConfig, next: ClashAPIConfig) => void;
};
-const { useRef } = React;
+const protocolOptions: { value: Protocol; label: string }[] = [
+ { value: 'http', label: 'HTTP' },
+ { value: 'https', label: 'HTTPS' },
+];
export default function APIConfig({
apiConfigs,
@@ -26,51 +35,162 @@ export default function APIConfig({
onAddConfig,
onRemoveConfig,
onSelectConfig,
+ onUpdateConfig,
}: Props) {
- const contentEl = useRef(null);
- const { baseURL, secret, errMsg, handleInputOnChange, handleContentOnKeyDown, onConfirm } =
- useBackendConfigForm({ onAddConfig });
+ const { t } = useTranslation();
+ const [showSecret, toggleSecret] = useToggle();
+ const {
+ protocol,
+ host,
+ port,
+ secret,
+ errMsg,
+ baseURLPreview,
+ isSubmitting,
+ editing,
+ startEdit,
+ cancelEdit,
+ handleProtocolOnChange,
+ handleInputOnChange,
+ handleContentOnKeyDown,
+ onConfirm,
+ } = useBackendConfigForm({ onAddConfig, onUpdateConfig });
+
+ const SecretIcon = showSecret ? EyeOff : Eye;
return (
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
- <div className={s0.root} ref={contentEl} onKeyDown={handleContentOnKeyDown}>
- <div className={s0.header}>
- <div className={s0.icon}>
- <SvgYacd width={160} height={160} stroke="var(--stroke)" />
+ // oxlint-disable-next-line jsx-a11y/no-static-element-interactions
+ <div className={s.root} onKeyDown={handleContentOnKeyDown}>
+ <header className={s.hero}>
+ <div className={s.logo}>
+ <SvgYacd width={96} height={96} stroke="var(--stroke)" />
</div>
- </div>
- <div className={s0.body}>
- <div className={s0.hostnamePort}>
- <Field
- id="baseURL"
- name="baseURL"
- label="API Base URL"
- type="text"
- placeholder="http://127.0.0.1:9090"
- value={baseURL}
- onChange={handleInputOnChange}
- />
- <Field
- id="secret"
- name="secret"
- label="Secret(optional)"
- value={secret}
- type="text"
- onChange={handleInputOnChange}
- />
+ <h1 className={s.title}>
+ {editing ? t('backend_form_title_edit') : t('backend_form_title')}
+ </h1>
+ <p className={s.desc}>{editing ? editing.baseURL : t('backend_form_desc')}</p>
+ </header>
+
+ <section className={s.card}>
+ <div className={s.grid}>
+ <div className={cx(s.field, s.protocol)}>
+ <span className={s.label} id="backend-protocol-label">
+ {t('protocol')}
+ </span>
+ <SegmentedControl
+ options={protocolOptions}
+ value={protocol}
+ onChange={handleProtocolOnChange}
+ label={t('protocol')}
+ className={s.protocolControl}
+ />
+ </div>
+
+ <div className={cx(s.field, s.host)}>
+ <label className={s.label} htmlFor="backend-host">
+ {t('host')}
+ </label>
+ <input
+ className={s.input}
+ id="backend-host"
+ name="host"
+ type="text"
+ autoComplete="off"
+ spellCheck={false}
+ placeholder="127.0.0.1"
+ value={host}
+ onChange={handleInputOnChange}
+ />
+ </div>
+
+ <div className={cx(s.field, s.port)}>
+ <label className={s.label} htmlFor="backend-port">
+ {t('port')}
+ </label>
+ <input
+ className={s.input}
+ id="backend-port"
+ name="port"
+ type="text"
+ inputMode="numeric"
+ autoComplete="off"
+ placeholder="9090"
+ value={port}
+ onChange={handleInputOnChange}
+ />
+ </div>
+
+ <div className={cx(s.field, s.secret)}>
+ <label className={s.label} htmlFor="backend-secret">
+ {t('secret')}
+ <span className={s.optional}>{t('optional')}</span>
+ </label>
+ <div className={s.inputWithAction}>
+ <input
+ className={s.input}
+ id="backend-secret"
+ name="secret"
+ type={showSecret ? 'text' : 'password'}
+ autoComplete="off"
+ spellCheck={false}
+ value={secret}
+ onChange={handleInputOnChange}
+ />
+ <button
+ type="button"
+ className={s.iconBtn}
+ onClick={toggleSecret}
+ title={showSecret ? t('hide_secret') : t('show_secret')}
+ aria-label={showSecret ? t('hide_secret') : t('show_secret')}
+ >
+ <SecretIcon size={16} />
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className={s.actions}>
+ <div className={s.status}>
+ {errMsg ? (
+ <span className={s.error}>
+ <AlertCircle size={14} />
+ {errMsg}
+ </span>
+ ) : baseURLPreview ? (
+ <span className={s.preview}>{baseURLPreview}</span>
+ ) : null}
+ </div>
+ <div className={s.buttons}>
+ {editing ? (
+ <Button kind="minimal" label={t('cancel_edit')} onClick={cancelEdit} />
+ ) : null}
+ <Button
+ label={editing ? t('save_backend') : t('add_backend')}
+ onClick={onConfirm}
+ isLoading={isSubmitting}
+ />
+ </div>
</div>
- </div>
- <div className={s0.error}>{errMsg ? errMsg : null}</div>
- <div className={s0.footer}>
- <Button label="Add" onClick={onConfirm} />
- </div>
- <div style={{ height: 20 }} />
- <BackendList
- apiConfigs={apiConfigs}
- selectedClashAPIConfigIndex={selectedClashAPIConfigIndex}
- onRemove={onRemoveConfig}
- onSelect={onSelectConfig}
- />
+ </section>
+
+ <section className={s.saved}>
+ <div className={s.savedHeader}>
+ <h2 className={s.savedTitle}>{t('saved_backends')}</h2>
+ {apiConfigs.length > 0 ? <span className={s.count}>{apiConfigs.length}</span> : null}
+ </div>
+ {apiConfigs.length > 0 ? (
+ <BackendList
+ apiConfigs={apiConfigs}
+ selectedClashAPIConfigIndex={selectedClashAPIConfigIndex}
+ editing={editing}
+ onRemove={onRemoveConfig}
+ onSelect={onSelectConfig}
+ onEdit={startEdit}
+ />
+ ) : (
+ <p className={s.empty}>{t('no_saved_backends')}</p>
+ )}
+ </section>
</div>
);
}
diff --git a/src/components/backend/APIDiscovery.module.scss b/src/components/backend/APIDiscovery.module.scss
index e2161f8..630ec36 100644
--- a/src/components/backend/APIDiscovery.module.scss
+++ b/src/components/backend/APIDiscovery.module.scss
@@ -6,7 +6,10 @@
left: 0;
right: 0;
- transform: none;
+ // Modal 用独立的 translate 属性做居中,这里必须同样用 translate 抵消。
+ // 注意别在同一条规则里再写 transform:postcss-preset-env 会把两者合并成
+ // transform: translate(0,0),translate 声明被吞掉,弹层就会偏移半屏。
+ translate: none;
padding: 0;
border-radius: 0;
diff --git a/src/components/backend/APIDiscovery.tsx b/src/components/backend/APIDiscovery.tsx
index f848623..aace980 100644
--- a/src/components/backend/APIDiscovery.tsx
+++ b/src/components/backend/APIDiscovery.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
-import Modal from '~/components/Modal';
+import Modal from '~/components/shared/Modal';
import { ThemeSwitcher } from '~/components/shared/ThemeSwitcher';
import { DOES_NOT_SUPPORT_FETCH, errors } from '~/misc/errors';
diff --git a/src/components/backend/BackendList.module.scss b/src/components/backend/BackendList.module.scss
index f3be8da..94d27c7 100644
--- a/src/components/backend/BackendList.module.scss
+++ b/src/components/backend/BackendList.module.scss
@@ -1,102 +1,156 @@
.ul {
- position: relative;
margin: 0;
padding: 0;
list-style: none;
- line-height: 1.8;
-
- --width-max-content: 230px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
}
.li {
- position: relative;
- margin: 5px 0;
- padding: 10px 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px 8px 4px;
border-radius: var(--border-radius);
- display: grid;
- place-content: center;
- grid-template-columns: 40px 1fr 40px;
- grid-template-rows: 30px;
- grid-template-areas: 'close url .';
- column-gap: 10px;
- border: 1px solid var(--bg-near-transparent);
+ background: var(--color-card);
+ border: 1px solid var(--color-card-border);
+ transition:
+ border-color 0.2s ease,
+ background-color 0.2s ease;
+
+ &:hover {
+ border-color: var(--color-focus-blue);
+ }
}
-.li:hover {
- background-color: var(--bg-near-transparent);
+.selected {
+ border-color: var(--color-focus-blue);
+ background: var(--color-node-active-bg);
}
-.close {
- opacity: 0;
- grid-area: close;
- place-self: center;
- cursor: pointer;
+/* 上方表单正绑着这一条 */
+.editing {
+ border-color: var(--color-focus-blue);
+ box-shadow: 0 0 0 3px var(--color-accent-soft-bg);
}
-.li:hover .close,
-.li:hover .eye {
- opacity: 1;
+/* 整行左侧是切换后端的点击区 */
+.main {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 8px;
+ appearance: none;
+ border: none;
+ border-radius: 8px;
+ background: transparent;
+ color: inherit;
+ font-family: inherit;
+ text-align: left;
+ cursor: pointer;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-focus-blue);
+ outline-offset: -2px;
+ }
}
-.close:focus,
-.eye:focus {
- opacity: 1;
+
+.dot {
+ flex-shrink: 0;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--color-text-secondary);
+ opacity: 0.4;
+
+ .selected & {
+ background: var(--color-success);
+ opacity: 1;
+ }
}
-.hasSecret {
- grid-template-rows: repeat(2, 30px);
- grid-template-areas:
- 'close url .'
- 'close secret eye';
+.body {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
}
.url {
- grid-area: url;
-}
-.secret {
- grid-area: secret;
-}
-.eye {
- grid-area: eye;
- opacity: 0;
- place-self: center;
- cursor: pointer;
+ font-size: 0.875rem;
+ color: var(--color-text-highlight);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.url,
.secret {
+ font-size: 0.72rem;
+ color: var(--color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
-.btn {
- outline: none;
- appearance: none;
- border: 1px solid transparent;
- background-color: transparent;
- color: inherit;
+.tools {
+ flex-shrink: 0;
display: flex;
align-items: center;
- padding: 5px;
- border-radius: 100px;
-}
-.btn:focus {
- border-color: var(--color-focus-blue);
-}
-.btn:hover:enabled {
- background-color: var(--color-focus-blue);
- color: white;
-}
-.btn:active:enabled {
- transform: scale(0.97);
+ gap: 4px;
}
-.btn:disabled {
- color: var(--color-text-secondary);
+
+.badge {
+ padding: 2px 8px;
+ border-radius: 100px;
+ background: var(--color-accent-soft-bg);
+ color: var(--color-accent-soft-fg);
+ font-size: 0.68rem;
+ font-weight: 500;
+ white-space: nowrap;
}
-.url {
+.iconBtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ appearance: none;
+ border: none;
+ border-radius: 8px;
+ background: transparent;
+ color: var(--color-icon);
cursor: pointer;
+ transition:
+ color 0.2s ease,
+ background-color 0.2s ease;
+
+ &:hover:enabled {
+ color: var(--color-text);
+ background: var(--color-hover-soft);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--color-focus-blue);
+ outline-offset: -2px;
+ }
+
+ &:disabled {
+ opacity: 0.35;
+ cursor: default;
+ }
}
-.url:hover {
- color: var(--color-text-highlight);
+
+.active {
+ color: var(--color-accent-soft-fg);
+ background: var(--color-accent-soft-bg);
+}
+
+.remove:hover:enabled {
+ color: var(--color-danger);
+ background: var(--color-danger-soft-bg);
}
diff --git a/src/components/backend/BackendList.tsx b/src/components/backend/BackendList.tsx
index 66fc319..1cbbe05 100644
--- a/src/components/backend/BackendList.tsx
+++ b/src/components/backend/BackendList.tsx
@@ -1,7 +1,8 @@
import cx from 'clsx';
import * as React from 'react';
+import { useTranslation } from 'react-i18next';
-import { Eye, EyeOff, X as Close } from '~/components/shared/FeatherIcons';
+import { Edit3, Eye, EyeOff, Trash2 } from '~/components/shared/FeatherIcons';
import { useToggle } from '~/hooks/basic';
import type { ClashAPIConfigWithAddedAt } from '~/store/types';
import type { ClashAPIConfig } from '~/types';
@@ -11,110 +12,111 @@ import s from './BackendList.module.scss';
type Props = {
apiConfigs: ClashAPIConfigWithAddedAt[];
selectedClashAPIConfigIndex: number;
+ editing: ClashAPIConfig | null;
onRemove: (x: ClashAPIConfig) => void;
onSelect: (x: ClashAPIConfig) => void;
+ onEdit: (x: ClashAPIConfig) => void;
};
export function BackendList({
apiConfigs,
selectedClashAPIConfigIndex,
+ editing,
onRemove,
onSelect,
+ onEdit,
}: Props) {
return (
- <>
- <ul className={s.ul}>
- {apiConfigs.map((item, idx) => {
- return (
- <li
- className={cx(s.li, {
- [s.hasSecret]: item.secret,
- [s.isSelected]: idx === selectedClashAPIConfigIndex,
- })}
- key={item.baseURL + item.secret}
- >
- <Item
- disableRemove={idx === selectedClashAPIConfigIndex}
- baseURL={item.baseURL}
- secret={item.secret}
- onRemove={onRemove}
- onSelect={onSelect}
- />
- </li>
- );
- })}
- </ul>
- </>
+ <ul className={s.ul}>
+ {apiConfigs.map((item, idx) => (
+ <Item
+ key={item.baseURL + item.secret}
+ baseURL={item.baseURL}
+ secret={item.secret}
+ isSelected={idx === selectedClashAPIConfigIndex}
+ isEditing={
+ editing != null && editing.baseURL === item.baseURL && editing.secret === item.secret
+ }
+ onRemove={onRemove}
+ onSelect={onSelect}
+ onEdit={onEdit}
+ />
+ ))}
+ </ul>
);
}
function Item({
baseURL,
secret,
- disableRemove,
+ isSelected,
+ isEditing,
onRemove,
onSelect,
+ onEdit,
}: {
baseURL: string;
secret?: string;
- disableRemove: boolean;
+ isSelected: boolean;
+ isEditing: boolean;
onRemove: (x: ClashAPIConfig) => void;
onSelect: (x: ClashAPIConfig) => void;
+ onEdit: (x: ClashAPIConfig) => void;
}) {
+ const { t } = useTranslation();
const [show, toggle] = useToggle();
- const Icon = show ? EyeOff : Eye;
-
- const handleTap = React.useCallback((e: React.KeyboardEvent) => {
- e.stopPropagation();
- }, []);
+ const SecretIcon = show ? EyeOff : Eye;
return (
- <>
- <Button
- disabled={disableRemove}
- onClick={() => onRemove({ baseURL, secret })}
- className={s.close}
- >
- <Close size={20} />
- </Button>
- <span
- className={s.url}
- tabIndex={0}
- role="button"
+ <li className={cx(s.li, { [s.selected]: isSelected, [s.editing]: isEditing })}>
+ <button
+ type="button"
+ className={s.main}
+ title={isSelected ? undefined : t('use_this_backend')}
onClick={() => onSelect({ baseURL, secret })}
- onKeyUp={handleTap}
>
- {baseURL}
- </span>
- <span />
- {secret ? (
- <>
- <span className={s.secret}>{show ? secret : '***'}</span>
-
- <Button onClick={toggle} className={s.eye}>
- <Icon size={20} />
- </Button>
- </>
- ) : null}
- </>
- );
-}
+ <span className={s.dot} />
+ <span className={s.body}>
+ <span className={s.url}>{baseURL}</span>
+ <span className={s.secret}>
+ {secret ? (show ? secret : '••••••••') : t('backend_no_secret')}
+ </span>
+ </span>
+ </button>
-function Button({
- children,
- onClick,
- className,
- disabled,
-}: {
- children: React.ReactNode;
-
- onClick?: (e: React.MouseEvent<HTMLButtonElement>) => unknown;
- className: string;
- disabled?: boolean;
-}) {
- return (
- <button disabled={disabled} className={cx(className, s.btn)} onClick={onClick}>
- {children}
- </button>
+ <div className={s.tools}>
+ {isSelected ? <span className={s.badge}>{t('backend_in_use')}</span> : null}
+ {secret ? (
+ <button
+ type="button"
+ className={s.iconBtn}
+ onClick={toggle}
+ title={show ? t('hide_secret') : t('show_secret')}
+ aria-label={show ? t('hide_secret') : t('show_secret')}
+ >
+ <SecretIcon size={16} />
+ </button>
+ ) : null}
+ <button
+ type="button"
+ className={cx(s.iconBtn, { [s.active]: isEditing })}
+ onClick={() => onEdit({ baseURL, secret })}
+ title={t('edit_backend')}
+ aria-label={t('edit_backend')}
+ >
+ <Edit3 size={16} />
+ </button>
+ <button
+ type="button"
+ className={cx(s.iconBtn, s.remove)}
+ disabled={isSelected}
+ onClick={() => onRemove({ baseURL, secret })}
+ title={t('remove_backend')}
+ aria-label={t('remove_backend')}
+ >
+ <Trash2 size={16} />
+ </button>
+ </div>
+ </li>
);
}