UI: доводка выпадашек игроков — без пустых вариантов, фон как у select, быстрее отклик

Из списков убраны варианты-плейсхолдеры («— игрок —» в выборе игрока,
«— фракция —» в выборе фракции — disabled+hidden, в закрытом виде текст
остаётся). Фон .suggest-list — surface-2, как у input/select; hover строк —
--border. Скорость: дебаунс поиска 250→100 мс, в инвайте показанный список
дофильтровывается по вводу на клиенте, не дожидаясь ответа сервера;
touch-action: manipulation на строках и триггере.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 03:13:05 +03:00
co-authored by Claude Fable 5
parent f22ee1ec29
commit 8a2b53d0b2
6 changed files with 20 additions and 32 deletions
+1 -9
View File
@@ -16,7 +16,7 @@ export function PlayerSelect({
}: { }: {
value: number | ""; value: number | "";
options: PlayerSuggestion[]; options: PlayerSuggestion[];
onPick: (userId: number | "") => void; onPick: (userId: number) => void;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const selected = value === "" ? null : options.find((o) => o.user_id === value) ?? null; const selected = value === "" ? null : options.find((o) => o.user_id === value) ?? null;
@@ -48,14 +48,6 @@ export function PlayerSelect({
onPick(u.user_id); onPick(u.user_id);
setOpen(false); setOpen(false);
}} }}
onClear={
selected
? () => {
onPick("");
setOpen(false);
}
: undefined
}
/> />
)} )}
</div> </div>
+1 -16
View File
@@ -14,28 +14,13 @@ export interface PlayerSuggestion {
export function PlayerSuggestions({ export function PlayerSuggestions({
items, items,
onPick, onPick,
onClear,
}: { }: {
items: PlayerSuggestion[]; items: PlayerSuggestion[];
onPick: (item: PlayerSuggestion) => void; onPick: (item: PlayerSuggestion) => void;
// Необязательная строка сброса сверху списка («— игрок —» в PlayerSelect).
onClear?: () => void;
}) { }) {
if (items.length === 0 && !onClear) return null; if (items.length === 0) return null;
return ( return (
<div className="suggest-list" role="listbox"> <div className="suggest-list" role="listbox">
{onClear && (
<button
type="button"
className="suggest-row suggest-clear"
onMouseDown={(e) => {
e.preventDefault();
onClear();
}}
>
— игрок —
</button>
)}
{items.map((u) => ( {items.map((u) => (
<button <button
key={u.user_id} key={u.user_id}
+3 -2
View File
@@ -4,8 +4,9 @@ import { useEffect, useState } from "react";
import { api, unwrap } from "../api/client"; import { api, unwrap } from "../api/client";
import { qk } from "../api/queryKeys"; import { qk } from "../api/queryKeys";
/** Значение с задержкой: не дёргаем поиск на каждый введённый символ. */ /** Значение с задержкой: не дёргаем поиск на каждый введённый символ.
function useDebounced<T>(value: T, ms = 250): T { * 100 мс — почти незаметно глазу, но склеивает быструю печать в один запрос. */
function useDebounced<T>(value: T, ms = 100): T {
const [debounced, setDebounced] = useState(value); const [debounced, setDebounced] = useState(value);
useEffect(() => { useEffect(() => {
const t = setTimeout(() => setDebounced(value), ms); const t = setTimeout(() => setDebounced(value), ms);
+5 -1
View File
@@ -215,7 +215,11 @@ export function CreateMatchPage() {
} }
style={{ flex: 1 }} style={{ flex: 1 }}
> >
<option value="">— фракция —</option> {/* disabled+hidden: в закрытом select это плейсхолдер, в раскрытом
списке варианта «— фракция —» нет. */}
<option value="" disabled hidden>
— фракция —
</option>
{(factions ?? []).map((f) => ( {(factions ?? []).map((f) => (
<option key={f.id} value={f.id}> <option key={f.id} value={f.id}>
{f.name_ru} {f.name_ru}
+6 -1
View File
@@ -57,8 +57,13 @@ export function GroupPage() {
}; };
// Подсказки: уже состоящих в группе не предлагаем, показываем до 3. // Подсказки: уже состоящих в группе не предлагаем, показываем до 3.
// Дофильтровка по текущему вводу — на клиенте: пока сервер отвечает на новую
// букву, показанный список (keepPreviousData) сужается мгновенно.
const memberIds = new Set((members ?? []).map((m) => m.user_id)); const memberIds = new Set((members ?? []).map((m) => m.user_id));
const suggestions = (foundUsers ?? []).filter((u) => !memberIds.has(u.user_id)).slice(0, 3); const needle = newNick.trim().toLowerCase();
const suggestions = (foundUsers ?? [])
.filter((u) => !memberIds.has(u.user_id) && u.nickname.toLowerCase().includes(needle))
.slice(0, 3);
const showSuggest = const showSuggest =
nickFocused && !nickPicked && newNick.trim().length > 0 && suggestions.length > 0; nickFocused && !nickPicked && newNick.trim().length > 0 && suggestions.length > 0;
+4 -3
View File
@@ -296,7 +296,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
top: calc(100% + 4px); top: calc(100% + 4px);
left: 0; left: 0;
right: 0; right: 0;
background: var(--surface); background: var(--surface-2); /* как у input/select — выпадашка продолжает поле */
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
@@ -317,11 +317,11 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
font-weight: 600; font-weight: 600;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
touch-action: manipulation; /* тап без задержки двойного тапа */
} }
.suggest-row:last-child { border-bottom: none; } .suggest-row:last-child { border-bottom: none; }
.suggest-row:hover, .suggest-row:active { background: var(--surface-2); } .suggest-row:hover, .suggest-row:active { background: var(--border); }
.suggest-row .nick { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .suggest-row .nick { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.suggest-clear { color: var(--muted); font-weight: 400; }
/* Кнопка-триггер PlayerSelect: выглядит как select, раскрывает .suggest-list. */ /* Кнопка-триггер PlayerSelect: выглядит как select, раскрывает .suggest-list. */
.picker-trigger { .picker-trigger {
@@ -339,6 +339,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
outline: none; outline: none;
touch-action: manipulation;
} }
.picker-trigger:focus { border-color: var(--accent); } .picker-trigger:focus { border-color: var(--accent); }
.picker-trigger .nick { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .picker-trigger .nick { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }