UI: единый Switch для булевых опций вместо смеси чекбоксов и свитчей

Новый компонент Switch (разметка .switch из global.css). Применён:
- создание группы: дополнения теперь свитчи, как в настройках группы;
- настройки группы: базовые дополнения показываются, как при создании
  (включены, заблокированы, подпись «всегда включено»), а не скрываются;
- админка: «Группировать по группам» и флаг «выбыл» в правке партии.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 17:38:46 +03:00
co-authored by Claude Fable 5
parent cbf3cfc6e6
commit d2c0feafcc
5 changed files with 48 additions and 33 deletions
+2 -3
View File
@@ -4,6 +4,7 @@ import { ApiError } from "../api/client";
import { useToast } from "../context/ToastContext"; import { useToast } from "../context/ToastContext";
import { useCreateGroup } from "../hooks/groups"; import { useCreateGroup } from "../hooks/groups";
import { useExpansions } from "../hooks/reference"; import { useExpansions } from "../hooks/reference";
import { Switch } from "./Switch";
export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) { export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
const { data: expansions } = useExpansions(); const { data: expansions } = useExpansions();
@@ -47,9 +48,7 @@ export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
{e.name_ru} {e.name_ru}
{e.is_base && <span className="muted small"> (всегда включено)</span>} {e.is_base && <span className="muted small"> (всегда включено)</span>}
</span> </span>
<input <Switch
type="checkbox"
style={{ width: "auto" }}
disabled={e.is_base} disabled={e.is_base}
checked={e.is_base || selected.has(e.id)} checked={e.is_base || selected.has(e.id)}
onChange={() => toggle(e.id)} onChange={() => toggle(e.id)}
+27
View File
@@ -0,0 +1,27 @@
/**
* Тумблер вкл/выкл (стили .switch в global.css) — единый контрол для булевых опций
* (дополнения группы, фильтры, флаги). Вместо нативных чекбоксов.
*/
export function Switch({
checked,
disabled,
onChange,
}: {
checked: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
}) {
return (
<span className="switch">
<input
type="checkbox"
checked={checked}
disabled={disabled}
onChange={(e) => onChange?.(e.target.checked)}
/>
<span className="switch-track">
<span className="switch-thumb" />
</span>
</span>
);
}
+10 -13
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { ApiError } from "../api/client"; import { ApiError } from "../api/client";
import { PlayerLink } from "../components/PlayerLink"; import { PlayerLink } from "../components/PlayerLink";
import { Spinner } from "../components/Spinner"; import { Spinner } from "../components/Spinner";
import { Switch } from "../components/Switch";
import { MAX_GROUP_SIZE } from "../domain/constants"; import { MAX_GROUP_SIZE } from "../domain/constants";
import { useToast } from "../context/ToastContext"; import { useToast } from "../context/ToastContext";
import { useMe } from "../hooks/auth"; import { useMe } from "../hooks/auth";
@@ -73,22 +74,18 @@ export function GroupSettingsPage() {
<div className="card"> <div className="card">
<h3>Дополнения</h3> <h3>Дополнения</h3>
<div className="stack"> <div className="stack">
{(expansions ?? []) {/* Базовые дополнения показываем, как и при создании группы: включены и заблокированы. */}
.filter((e) => !e.is_base) {(expansions ?? []).map((e) => (
.map((e) => (
<label key={e.id} className="row" style={{ justifyContent: "space-between" }}> <label key={e.id} className="row" style={{ justifyContent: "space-between" }}>
<span>{e.name_ru}</span> <span>
<span className="switch"> {e.name_ru}
<input {e.is_base && <span className="muted small"> (всегда включено)</span>}
type="checkbox" </span>
disabled={!isOwner} <Switch
checked={selected.has(e.id)} disabled={e.is_base || !isOwner}
checked={e.is_base || selected.has(e.id)}
onChange={() => toggle(e.id)} onChange={() => toggle(e.id)}
/> />
<span className="switch-track">
<span className="switch-thumb" />
</span>
</span>
</label> </label>
))} ))}
</div> </div>
+2 -6
View File
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { ApiError } from "../../api/client"; import { ApiError } from "../../api/client";
import { MatchMedia } from "../../components/MatchMedia"; import { MatchMedia } from "../../components/MatchMedia";
import { Spinner } from "../../components/Spinner"; import { Spinner } from "../../components/Spinner";
import { Switch } from "../../components/Switch";
import { WIN_REASONS, type WinReason } from "../../domain/winReasons"; import { WIN_REASONS, type WinReason } from "../../domain/winReasons";
import { useToast } from "../../context/ToastContext"; import { useToast } from "../../context/ToastContext";
import { import {
@@ -141,12 +142,7 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
/> />
</div> </div>
<label className="row small" style={{ flex: "0 0 auto", gap: 4 }}> <label className="row small" style={{ flex: "0 0 auto", gap: 4 }}>
<input <Switch checked={r.eliminated} onChange={(v) => upd(i, { eliminated: v })} />
type="checkbox"
style={{ width: "auto" }}
checked={r.eliminated}
onChange={(e) => upd(i, { eliminated: e.target.checked })}
/>
выбыл выбыл
</label> </label>
</div> </div>
@@ -2,6 +2,7 @@ import { Pencil, Trash2 } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Spinner } from "../../components/Spinner"; import { Spinner } from "../../components/Spinner";
import { Switch } from "../../components/Switch";
import { useToast } from "../../context/ToastContext"; import { useToast } from "../../context/ToastContext";
import { import {
useAdminDeleteMatch, useAdminDeleteMatch,
@@ -112,12 +113,7 @@ export function AdminRecordsPage() {
))} ))}
</select> </select>
<label className="row" style={{ gap: 8 }}> <label className="row" style={{ gap: 8 }}>
<input <Switch checked={grouped} onChange={setGrouped} />
type="checkbox"
style={{ width: "auto" }}
checked={grouped}
onChange={(e) => setGrouped(e.target.checked)}
/>
Группировать по группам Группировать по группам
</label> </label>
</div> </div>