feat: Android download page, analysis retry and a collapsible search
ci / api / api (push) Successful in 37s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 46s
ci / db / db (push) Failing after 14s
ci / recommender / recommender (push) Successful in 51s
ci / ts / ts (packages + web) (push) Successful in 57s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m2s

- /android is a public page with the latest APK, the server address to copy
  and three install steps; linked from the sign-in screen and the profile menu
- failed audio analysis can be retried from the library page, which also shows
  the most common error; the analyzer no longer burns attempts on files that
  are missing because the disk is not mounted
- the top bar search folds into a round button and slides open on click, "/"
  or Ctrl+K

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 23:55:51 +03:00
1 parent 2d49cb3f19
commit fda6ed48ab
27 files changed
+757 -277

No files matched your search

+3 -1
View File
@@ -22,6 +22,7 @@ import {
dislikeFillIcon,
dislikeIcon,
settingsIcon,
androidIcon,
storageFillIcon,
storageIcon, inboxFillIcon, inboxIcon, myUploadsFillIcon, myUploadsIcon, uploadFillIcon, uploadIcon, tuneFillIcon, tuneIcon} from '../shared/ui/icons';
import { Avatar, cx, Icon, Logo, Menu } from '../shared/ui/ui';
@@ -102,8 +103,8 @@ function Shell() {
<Link to="/" className={s.logoLink}>
<Logo />
</Link>
<SearchBox />
<div className={s.topRight}>
<SearchBox />
<Menu
label={t('profile.menu')}
triggerClassName={s.profileButton}
@@ -122,6 +123,7 @@ function Shell() {
}
items={[
{ label: t('profile.settings'), icon: settingsIcon, onSelect: () => void navigate({ to: '/settings' }) },
{ label: t('profile.android_app'), icon: androidIcon, onSelect: () => void navigate({ to: '/android' }) },
{ label: t('profile.logout'), icon: logoutIcon, danger: true, onSelect: () => void session.logout() },
]}
/>
+33 -5
View File
@@ -1,12 +1,17 @@
.box {
position: relative;
flex: 1;
max-width: 520px;
flex: none;
width: 40px;
transition: width 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.expanded {
width: min(520px, 46vw);
}
.icon {
position: absolute;
left: 14px;
left: 11px;
top: 50%;
transform: translateY(-50%);
color: var(--text3);
@@ -16,13 +21,36 @@
.input {
width: 100%;
height: 40px;
padding: 0 40px 0 42px;
padding: 0 40px 0 40px;
border-radius: var(--radius-pill);
border: 1px solid var(--line);
background: var(--surface2);
color: var(--text);
font: inherit;
outline: none;
transition:
border-color 0.2s,
box-shadow 0.2s,
background 0.2s;
}
/* Свёрнутое поле — круглая кнопка: без подсказки и курсора ввода. */
.box:not(.expanded) .input {
padding: 0;
cursor: pointer;
color: transparent;
}
.box:not(.expanded) .input::placeholder {
color: transparent;
}
.box:not(.expanded) .input:hover {
background: var(--surface3);
}
.box:not(.expanded) .kbd {
display: none;
}
.input:focus {
@@ -56,8 +84,8 @@
.popover {
position: absolute;
left: 0;
right: 0;
width: min(520px, calc(100vw - 32px));
top: calc(100% + 8px);
z-index: 45;
max-height: min(70vh, 560px);
+16 -4
View File
@@ -23,7 +23,10 @@ type Item = {
go: () => void;
};
/** Поиск в топбаре: подсказки по мере ввода, Enter — страница поиска или выбранный элемент. */
/**
* Поиск в топбаре: подсказки по мере ввода, Enter — страница поиска или выбранный элемент.
* Без фокуса и текста свёрнут в кнопку с лупой; поле раскрывается влево с анимацией.
*/
export function SearchBox() {
const { t } = useTranslation();
const session = useSession();
@@ -34,6 +37,9 @@ export function SearchBox() {
const [debounced, setDebounced] = useState('');
const [open, setOpen] = useState(false);
const [active, setActive] = useState(-1);
const [focused, setFocused] = useState(false);
// Свёрнут в круглую кнопку, пока не нужен: раскрывается по клику, «/» или Ctrl+K.
const expanded = focused || q !== '';
useEffect(() => {
const id = setTimeout(() => setDebounced(q.trim()), DEBOUNCE_MS);
@@ -141,7 +147,7 @@ export function SearchBox() {
const optionId = (i: number) => `${listId}-${i}`;
return (
<div className={s.box}>
<div className={cx(s.box, expanded && s.expanded)}>
<Icon src={searchIcon} size={18} className={s.icon} />
<input
ref={inputRef}
@@ -160,8 +166,14 @@ export function SearchBox() {
setOpen(true);
setActive(-1);
}}
onFocus={() => setOpen(true)}
onBlur={() => setTimeout(close, 120)}
onFocus={() => {
setFocused(true);
setOpen(true);
}}
onBlur={() => {
setFocused(false);
setTimeout(close, 120);
}}
onKeyDown={onKeyDown}
/>
<kbd className={s.kbd} aria-hidden>