Refine server picker toggle and bump Harbor versions
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 15s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-12 14:31:34 +03:00
parent d9745e9aed
commit fca3c0b705
4 changed files with 215 additions and 113 deletions

View File

@@ -181,118 +181,128 @@ export function ServerPicker({
/>
));
if (!advanced) {
const simpleServers = [
...(selected ? [selected] : []),
...servers.filter(({ id }) => id !== selectedServerId),
].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>;
}
const simpleServers = [
...(selected ? [selected] : []),
...servers.filter(({ id }) => id !== selectedServerId),
].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"
className={`client-server-mode-toggle${advanced ? ' is-open' : ''}`}
type="button"
aria-expanded="true"
onClick={() => setAdvanced(false)}
>Простой список</button>
<div className="client-server-tools">
<input
type="search"
value={query}
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>
aria-expanded={advanced}
aria-label={advanced ? 'Скрыть поиск и фильтры' : 'Показать поиск и фильтры'}
onClick={() => setAdvanced((current) => !current)}
>
<span>Поиск и фильтры</span>
<svg viewBox="0 0 12 8" aria-hidden="true"><path d="m1 1 5 5 5-5" /></svg>
</button>
<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)}
<div className="client-server-mode-panels">
<div
className={`client-server-mode-panel is-simple${advanced ? '' : ' is-open'}`}
aria-hidden={advanced}
inert={advanced ? true : undefined}
>
<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-mode-panel-inner">
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`simple:${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 && <p className="client-server-overflow-note">
Ещё {servers.length - simpleServers.length} доступны через поиск
</p>}
</div>
</div>
</div>
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`${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
className={`client-server-mode-panel is-advanced${advanced ? ' is-open' : ''}`}
aria-hidden={!advanced}
inert={!advanced ? true : undefined}
>
<div className="client-server-mode-panel-inner">
<div className="client-server-tools">
<input
type="search"
value={query}
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">
<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>
</section>;
}