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:
@@ -16,7 +16,7 @@ export function PlayerSelect({
|
||||
}: {
|
||||
value: number | "";
|
||||
options: PlayerSuggestion[];
|
||||
onPick: (userId: number | "") => void;
|
||||
onPick: (userId: number) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selected = value === "" ? null : options.find((o) => o.user_id === value) ?? null;
|
||||
@@ -48,14 +48,6 @@ export function PlayerSelect({
|
||||
onPick(u.user_id);
|
||||
setOpen(false);
|
||||
}}
|
||||
onClear={
|
||||
selected
|
||||
? () => {
|
||||
onPick("");
|
||||
setOpen(false);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,28 +14,13 @@ export interface PlayerSuggestion {
|
||||
export function PlayerSuggestions({
|
||||
items,
|
||||
onPick,
|
||||
onClear,
|
||||
}: {
|
||||
items: PlayerSuggestion[];
|
||||
onPick: (item: PlayerSuggestion) => void;
|
||||
// Необязательная строка сброса сверху списка («— игрок —» в PlayerSelect).
|
||||
onClear?: () => void;
|
||||
}) {
|
||||
if (items.length === 0 && !onClear) return null;
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div className="suggest-list" role="listbox">
|
||||
{onClear && (
|
||||
<button
|
||||
type="button"
|
||||
className="suggest-row suggest-clear"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
onClear();
|
||||
}}
|
||||
>
|
||||
— игрок —
|
||||
</button>
|
||||
)}
|
||||
{items.map((u) => (
|
||||
<button
|
||||
key={u.user_id}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useEffect, useState } from "react";
|
||||
import { api, unwrap } from "../api/client";
|
||||
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);
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setDebounced(value), ms);
|
||||
|
||||
@@ -215,7 +215,11 @@ export function CreateMatchPage() {
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<option value="">— фракция —</option>
|
||||
{/* disabled+hidden: в закрытом select это плейсхолдер, в раскрытом
|
||||
списке варианта «— фракция —» нет. */}
|
||||
<option value="" disabled hidden>
|
||||
— фракция —
|
||||
</option>
|
||||
{(factions ?? []).map((f) => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.name_ru}
|
||||
|
||||
@@ -57,8 +57,13 @@ export function GroupPage() {
|
||||
};
|
||||
|
||||
// Подсказки: уже состоящих в группе не предлагаем, показываем до 3.
|
||||
// Дофильтровка по текущему вводу — на клиенте: пока сервер отвечает на новую
|
||||
// букву, показанный список (keepPreviousData) сужается мгновенно.
|
||||
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 =
|
||||
nickFocused && !nickPicked && newNick.trim().length > 0 && suggestions.length > 0;
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--surface);
|
||||
background: var(--surface-2); /* как у input/select — выпадашка продолжает поле */
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
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;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation; /* тап без задержки двойного тапа */
|
||||
}
|
||||
.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-clear { color: var(--muted); font-weight: 400; }
|
||||
|
||||
/* Кнопка-триггер PlayerSelect: выглядит как select, раскрывает .suggest-list. */
|
||||
.picker-trigger {
|
||||
@@ -339,6 +339,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.picker-trigger:focus { border-color: var(--accent); }
|
||||
.picker-trigger .nick { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
Reference in New Issue
Block a user