diff --git a/src/shared/versions.js b/src/shared/versions.js index 8eedfd2..f0596e6 100644 --- a/src/shared/versions.js +++ b/src/shared/versions.js @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.7.13', - gatewayClient: '0.7.13', + macClient: '0.7.14', + gatewayClient: '0.7.14', gatewayBackend: '0.7.1', }); diff --git a/src/web/components/ServerPicker.jsx b/src/web/components/ServerPicker.jsx index 0eda7b9..9c5899d 100644 --- a/src/web/components/ServerPicker.jsx +++ b/src/web/components/ServerPicker.jsx @@ -181,118 +181,128 @@ export function ServerPicker({ /> )); - if (!advanced) { - const simpleServers = [ - ...(selected ? [selected] : []), - ...servers.filter(({ id }) => id !== selectedServerId), - ].slice(0, SERVER_RESULT_WINDOW); - - return
- {prompt && Выберите сервер} - -
-
- {simpleServers.map((server, index) => )} -
- {servers.length > simpleServers.length && } -
-
; - } + const simpleServers = [ + ...(selected ? [selected] : []), + ...servers.filter(({ id }) => id !== selectedServerId), + ].slice(0, SERVER_RESULT_WINDOW); return
{prompt && Выберите сервер} -
- setQuery(event.target.value)} - /> -
- {[ - ['all', 'Все'], - ['favorites', '★'], - ['recent', 'Недавние'], - ].map(([id, label]) => )} - -
-
+ aria-expanded={advanced} + aria-label={advanced ? 'Скрыть поиск и фильтры' : 'Показать поиск и фильтры'} + onClick={() => setAdvanced((current) => !current)} + > + Поиск и фильтры + + -
- - {selected && !autoActive && } -
+
+
+
+ {simpleServers.map((server, index) => )} +
+ {servers.length > simpleServers.length &&

+ Ещё {servers.length - simpleServers.length} — доступны через поиск +

} +
+
+ -
- {!visible.length &&

Серверы не найдены

} - {grouped ? groupServers(visible).map(([group, items]) => { - const isCollapsed = collapsed.includes(group); - return
- - {!isCollapsed &&
{renderRows(items)}
} -
; - }) :
{renderRows(visible)}
} - {pageCount > 1 && } +
+
+
+ setQuery(event.target.value)} + /> +
+ {[ + ['all', 'Все'], + ['favorites', '★'], + ['recent', 'Недавние'], + ].map(([id, label]) => )} + +
+
+ +
+ + {selected && !autoActive && } +
+ +
+ {!visible.length &&

Серверы не найдены

} + {grouped ? groupServers(visible).map(([group, items]) => { + const isCollapsed = collapsed.includes(group); + return
+ + {!isCollapsed &&
{renderRows(items)}
} +
; + }) :
{renderRows(visible)}
} + {pageCount > 1 && } +
+
+
; } diff --git a/src/web/styles.css b/src/web/styles.css index 2366390..900e946 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -2708,7 +2708,10 @@ p { .client-server-mode-toggle { min-height: 32px; - display: block; + display: flex; + align-items: center; + justify-content: center; + gap: 7px; margin: 0 auto 8px; padding: 5px 8px; border: 0; @@ -2716,14 +2719,94 @@ p { color: var(--client-muted); font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace; 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); + text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 38%, transparent); } -.client-server-scroll > .client-server-mode-toggle { - margin-top: 8px; +.client-server-mode-toggle svg { + 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 { @@ -3398,6 +3481,11 @@ p { .client-server, .client-server-auto, .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-group-toggle, .client-server-more, diff --git a/test/web/server-picker.test.js b/test/web/server-picker.test.js index 4730b76..802e60f 100644 --- a/test/web/server-picker.test.js +++ b/test/web/server-picker.test.js @@ -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', () => { assert.match(picker, /const \[advanced, setAdvanced\] = useState\(false\)/); - assert.match(picker, /if \(!advanced\)/); - assert.match(picker, />Поиск и фильтры<\/button>/); - assert.match(picker, />Простой список<\/button>/); + assert.match(picker, /aria-expanded={advanced}/); + assert.match(picker, /setAdvanced\(\(current\) => !current\)/); + assert.match(picker, /Поиск и фильтры<\/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\)/); });