Refine server picker toggle and bump Harbor versions
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.7.13',
|
macClient: '0.7.14',
|
||||||
gatewayClient: '0.7.13',
|
gatewayClient: '0.7.14',
|
||||||
gatewayBackend: '0.7.1',
|
gatewayBackend: '0.7.1',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -181,118 +181,128 @@ export function ServerPicker({
|
|||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!advanced) {
|
const simpleServers = [
|
||||||
const simpleServers = [
|
...(selected ? [selected] : []),
|
||||||
...(selected ? [selected] : []),
|
...servers.filter(({ id }) => id !== selectedServerId),
|
||||||
...servers.filter(({ id }) => id !== selectedServerId),
|
].slice(0, SERVER_RESULT_WINDOW);
|
||||||
].slice(0, SERVER_RESULT_WINDOW);
|
|
||||||
|
|
||||||
return <section className="client-servers is-scalable" aria-label="Выберите сервер">
|
|
||||||
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
|
||||||
<button
|
|
||||||
className="client-server-mode-toggle"
|
|
||||||
type="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
onClick={() => setAdvanced(true)}
|
|
||||||
>Поиск и фильтры</button>
|
|
||||||
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`${serverKey}:${revealVersion}`}>
|
|
||||||
<div className="client-server-grid">
|
|
||||||
{simpleServers.map((server, index) => <ServerRow
|
|
||||||
key={server.id}
|
|
||||||
server={server}
|
|
||||||
selected={server.id === selectedServerId}
|
|
||||||
disabled={disabled}
|
|
||||||
index={index}
|
|
||||||
onSelect={select}
|
|
||||||
/>)}
|
|
||||||
</div>
|
|
||||||
{servers.length > simpleServers.length && <button
|
|
||||||
className="client-server-mode-toggle"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAdvanced(true)}
|
|
||||||
>Ещё {servers.length - simpleServers.length} — открыть поиск</button>}
|
|
||||||
</div>
|
|
||||||
</section>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <section className="client-servers is-scalable" aria-label="Выберите сервер">
|
return <section className="client-servers is-scalable" aria-label="Выберите сервер">
|
||||||
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
||||||
<button
|
<button
|
||||||
className="client-server-mode-toggle"
|
className={`client-server-mode-toggle${advanced ? ' is-open' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded="true"
|
aria-expanded={advanced}
|
||||||
onClick={() => setAdvanced(false)}
|
aria-label={advanced ? 'Скрыть поиск и фильтры' : 'Показать поиск и фильтры'}
|
||||||
>Простой список</button>
|
onClick={() => setAdvanced((current) => !current)}
|
||||||
<div className="client-server-tools">
|
>
|
||||||
<input
|
<span>Поиск и фильтры</span>
|
||||||
type="search"
|
<svg viewBox="0 0 12 8" aria-hidden="true"><path d="m1 1 5 5 5-5" /></svg>
|
||||||
value={query}
|
</button>
|
||||||
aria-label="Найти сервер"
|
|
||||||
placeholder="Поиск сервера"
|
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="client-server-filters" aria-label="Фильтр серверов">
|
|
||||||
{[
|
|
||||||
['all', 'Все'],
|
|
||||||
['favorites', '★'],
|
|
||||||
['recent', 'Недавние'],
|
|
||||||
].map(([id, label]) => <button
|
|
||||||
type="button"
|
|
||||||
className={view === id ? 'is-active' : ''}
|
|
||||||
aria-pressed={view === id}
|
|
||||||
key={id}
|
|
||||||
onClick={() => setView(id)}
|
|
||||||
>{label}</button>)}
|
|
||||||
<button type="button" disabled={checking || !visible.length} onClick={checkVisible}>
|
|
||||||
{checking ? 'Проверяем…' : 'Проверить TCP'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="client-server-pinned">
|
<div className="client-server-mode-panels">
|
||||||
<button
|
<div
|
||||||
className={`client-server-auto${autoActive ? ' is-selected' : ''}`}
|
className={`client-server-mode-panel is-simple${advanced ? '' : ' is-open'}`}
|
||||||
type="button"
|
aria-hidden={advanced}
|
||||||
aria-pressed={autoActive}
|
inert={advanced ? true : undefined}
|
||||||
disabled={disabled || !servers.length}
|
|
||||||
onClick={() => select(autoServer(servers)?.id, true)}
|
|
||||||
>
|
>
|
||||||
<strong>Auto</strong>
|
<div className="client-server-mode-panel-inner">
|
||||||
<small>Первый стабильный сервер</small>
|
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`simple:${serverKey}:${revealVersion}`}>
|
||||||
</button>
|
<div className="client-server-grid">
|
||||||
{selected && !autoActive && <ServerRow
|
{simpleServers.map((server, index) => <ServerRow
|
||||||
server={selected}
|
key={server.id}
|
||||||
selected
|
server={server}
|
||||||
favorite={favorites.includes(selected.id)}
|
selected={server.id === selectedServerId}
|
||||||
ping={pings[selected.id]}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
index={index}
|
||||||
index={0}
|
onSelect={select}
|
||||||
onSelect={select}
|
/>)}
|
||||||
onFavorite={toggleFavorite}
|
</div>
|
||||||
/>}
|
{servers.length > simpleServers.length && <p className="client-server-overflow-note">
|
||||||
</div>
|
Ещё {servers.length - simpleServers.length} — доступны через поиск
|
||||||
|
</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`${serverKey}:${revealVersion}`}>
|
<div
|
||||||
{!visible.length && <p className="client-server-empty">Серверы не найдены</p>}
|
className={`client-server-mode-panel is-advanced${advanced ? ' is-open' : ''}`}
|
||||||
{grouped ? groupServers(visible).map(([group, items]) => {
|
aria-hidden={!advanced}
|
||||||
const isCollapsed = collapsed.includes(group);
|
inert={!advanced ? true : undefined}
|
||||||
return <section className="client-server-group" key={group}>
|
>
|
||||||
<button
|
<div className="client-server-mode-panel-inner">
|
||||||
className="client-server-group-toggle"
|
<div className="client-server-tools">
|
||||||
type="button"
|
<input
|
||||||
aria-expanded={!isCollapsed}
|
type="search"
|
||||||
onClick={() => setCollapsed((current) => current.includes(group)
|
value={query}
|
||||||
? current.filter((item) => item !== group)
|
aria-label="Найти сервер"
|
||||||
: [...current, group])}
|
placeholder="Поиск сервера"
|
||||||
>{group} <small>{items.length}</small></button>
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
{!isCollapsed && <div className="client-server-grid">{renderRows(items)}</div>}
|
/>
|
||||||
</section>;
|
<div className="client-server-filters" aria-label="Фильтр серверов">
|
||||||
}) : <div className="client-server-grid">{renderRows(visible)}</div>}
|
{[
|
||||||
{pageCount > 1 && <nav className="client-server-pages" aria-label="Страницы серверов">
|
['all', 'Все'],
|
||||||
<button className="client-server-more" type="button" disabled={page === 0} onClick={() => setPage((current) => current - 1)}>Назад</button>
|
['favorites', '★'],
|
||||||
<span>{page + 1} / {pageCount}</span>
|
['recent', 'Недавние'],
|
||||||
<button className="client-server-more" type="button" disabled={page + 1 === pageCount} onClick={() => setPage((current) => current + 1)}>Дальше</button>
|
].map(([id, label]) => <button
|
||||||
</nav>}
|
type="button"
|
||||||
|
className={view === id ? 'is-active' : ''}
|
||||||
|
aria-pressed={view === id}
|
||||||
|
key={id}
|
||||||
|
onClick={() => setView(id)}
|
||||||
|
>{label}</button>)}
|
||||||
|
<button type="button" disabled={checking || !visible.length} onClick={checkVisible}>
|
||||||
|
{checking ? 'Проверяем…' : 'Проверить TCP'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="client-server-pinned">
|
||||||
|
<button
|
||||||
|
className={`client-server-auto${autoActive ? ' is-selected' : ''}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={autoActive}
|
||||||
|
disabled={disabled || !servers.length}
|
||||||
|
onClick={() => select(autoServer(servers)?.id, true)}
|
||||||
|
>
|
||||||
|
<strong>Auto</strong>
|
||||||
|
<small>Первый стабильный сервер</small>
|
||||||
|
</button>
|
||||||
|
{selected && !autoActive && <ServerRow
|
||||||
|
server={selected}
|
||||||
|
selected
|
||||||
|
favorite={favorites.includes(selected.id)}
|
||||||
|
ping={pings[selected.id]}
|
||||||
|
disabled={disabled}
|
||||||
|
index={0}
|
||||||
|
onSelect={select}
|
||||||
|
onFavorite={toggleFavorite}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`advanced:${serverKey}:${revealVersion}`}>
|
||||||
|
{!visible.length && <p className="client-server-empty">Серверы не найдены</p>}
|
||||||
|
{grouped ? groupServers(visible).map(([group, items]) => {
|
||||||
|
const isCollapsed = collapsed.includes(group);
|
||||||
|
return <section className="client-server-group" key={group}>
|
||||||
|
<button
|
||||||
|
className="client-server-group-toggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded={!isCollapsed}
|
||||||
|
onClick={() => setCollapsed((current) => current.includes(group)
|
||||||
|
? current.filter((item) => item !== group)
|
||||||
|
: [...current, group])}
|
||||||
|
>{group} <small>{items.length}</small></button>
|
||||||
|
{!isCollapsed && <div className="client-server-grid">{renderRows(items)}</div>}
|
||||||
|
</section>;
|
||||||
|
}) : <div className="client-server-grid">{renderRows(visible)}</div>}
|
||||||
|
{pageCount > 1 && <nav className="client-server-pages" aria-label="Страницы серверов">
|
||||||
|
<button className="client-server-more" type="button" disabled={page === 0} onClick={() => setPage((current) => current - 1)}>Назад</button>
|
||||||
|
<span>{page + 1} / {pageCount}</span>
|
||||||
|
<button className="client-server-more" type="button" disabled={page + 1 === pageCount} onClick={() => setPage((current) => current + 1)}>Дальше</button>
|
||||||
|
</nav>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2708,7 +2708,10 @@ p {
|
|||||||
|
|
||||||
.client-server-mode-toggle {
|
.client-server-mode-toggle {
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
margin: 0 auto 8px;
|
margin: 0 auto 8px;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -2716,14 +2719,94 @@ p {
|
|||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: color 220ms ease, text-shadow 320ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-mode-toggle:hover {
|
.client-server-mode-toggle:hover,
|
||||||
|
.client-server-mode-toggle.is-open {
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
|
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 38%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-scroll > .client-server-mode-toggle {
|
.client-server-mode-toggle svg {
|
||||||
margin-top: 8px;
|
width: 10px;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
transition: transform 480ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-toggle.is-open svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panels {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
filter: blur(7px);
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition:
|
||||||
|
grid-template-rows 620ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
opacity 260ms ease,
|
||||||
|
filter 420ms ease,
|
||||||
|
transform 520ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
visibility 0s 620ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel.is-open {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
filter: blur(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel-inner {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel.is-advanced .client-server-tools,
|
||||||
|
.client-server-mode-panel.is-advanced .client-server-pinned,
|
||||||
|
.client-server-mode-panel.is-advanced .client-server-scroll {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(5px);
|
||||||
|
transform: translateY(-8px);
|
||||||
|
transition: opacity 300ms ease, filter 440ms ease, transform 520ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel.is-advanced.is-open .client-server-tools,
|
||||||
|
.client-server-mode-panel.is-advanced.is-open .client-server-pinned,
|
||||||
|
.client-server-mode-panel.is-advanced.is-open .client-server-scroll {
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel.is-advanced.is-open .client-server-tools {
|
||||||
|
transition-delay: 90ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel.is-advanced.is-open .client-server-pinned {
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel.is-advanced.is-open .client-server-scroll {
|
||||||
|
transition-delay: 210ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-overflow-note {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--client-muted);
|
||||||
|
font-size: 9px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-tools input {
|
.client-server-tools input {
|
||||||
@@ -3398,6 +3481,11 @@ p {
|
|||||||
.client-server,
|
.client-server,
|
||||||
.client-server-auto,
|
.client-server-auto,
|
||||||
.client-server-mode-toggle,
|
.client-server-mode-toggle,
|
||||||
|
.client-server-mode-toggle svg,
|
||||||
|
.client-server-mode-panel,
|
||||||
|
.client-server-mode-panel .client-server-tools,
|
||||||
|
.client-server-mode-panel .client-server-pinned,
|
||||||
|
.client-server-mode-panel .client-server-scroll,
|
||||||
.client-server-favorite,
|
.client-server-favorite,
|
||||||
.client-server-group-toggle,
|
.client-server-group-toggle,
|
||||||
.client-server-more,
|
.client-server-more,
|
||||||
|
|||||||
@@ -50,8 +50,12 @@ test('server picker keeps health manual and the rendered result window bounded',
|
|||||||
|
|
||||||
test('server picker starts simple and reveals advanced controls on demand', () => {
|
test('server picker starts simple and reveals advanced controls on demand', () => {
|
||||||
assert.match(picker, /const \[advanced, setAdvanced\] = useState\(false\)/);
|
assert.match(picker, /const \[advanced, setAdvanced\] = useState\(false\)/);
|
||||||
assert.match(picker, /if \(!advanced\)/);
|
assert.match(picker, /aria-expanded={advanced}/);
|
||||||
assert.match(picker, />Поиск и фильтры<\/button>/);
|
assert.match(picker, /setAdvanced\(\(current\) => !current\)/);
|
||||||
assert.match(picker, />Простой список<\/button>/);
|
assert.match(picker, /<span>Поиск и фильтры<\/span>/);
|
||||||
|
assert.match(picker, /client-server-mode-panel is-simple/);
|
||||||
|
assert.match(picker, /client-server-mode-panel is-advanced/);
|
||||||
|
assert.match(picker, /inert={advanced \? true : undefined}/);
|
||||||
|
assert.match(picker, /inert={!advanced \? true : undefined}/);
|
||||||
assert.match(picker, /\.slice\(0, SERVER_RESULT_WINDOW\)/);
|
assert.match(picker, /\.slice\(0, SERVER_RESULT_WINDOW\)/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user