feat(instance): сервер установщика больше не «главный»

Понятие главного сервера убрано из поведения: сервер, создаваемый установщиком,
обычный (is_main = false), автовступления в него нет, а в настройках инстанса
он остаётся лишь справочно (main_guild_id → AdminGuildID в ответе API).
Клиент от него больше не зависит: новичок начинает с пустым списком серверов.
This commit is contained in:
2026-09-20 01:12:01 +03:00
parent 33dc1fec96
commit f26728113d
9 changed files with 458 additions and 23 deletions
+98
View File
@@ -0,0 +1,98 @@
import { useEffect, useRef, type ReactNode, type RefObject } from 'react';
interface PopoverProps {
open: boolean;
/** Доступное имя панели (озвучивается скринридером). */
label: string;
onClose: () => void;
children: ReactNode;
/** Элемент-якорь: клик по нему не считается кликом «вне панели». */
anchorRef?: RefObject<HTMLElement | null>;
/** Куда раскрывается панель: вверх (по умолчанию) или вниз. */
placement?: 'top' | 'bottom';
align?: 'left' | 'right';
className?: string;
}
/**
* Всплывающая панель произвольного содержимого (`role="dialog"`): в отличие от
* `Menu` внутри неё могут быть слайдеры и ссылки, а не только пункты меню.
*
* Закрывается по Escape и клику вне панели — как меню; позиционирование задаёт
* родитель (панель абсолютная, по умолчанию растёт вверх от нижней панели).
*/
export function Popover({
open,
label,
onClose,
children,
anchorRef,
placement = 'top',
align = 'left',
className = '',
}: PopoverProps) {
const panelRef = useRef<HTMLDivElement | null>(null);
const onCloseRef = useRef(onClose);
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
// Клик вне панели (и вне якоря) закрывает её.
useEffect(() => {
if (!open) {
return;
}
const onPointerDown = (event: Event): void => {
const target = event.target;
if (!(target instanceof Node)) {
return;
}
if (panelRef.current?.contains(target) === true) {
return;
}
if (anchorRef?.current?.contains(target) === true) {
return;
}
onCloseRef.current();
};
document.addEventListener('mousedown', onPointerDown);
document.addEventListener('touchstart', onPointerDown);
return () => {
document.removeEventListener('mousedown', onPointerDown);
document.removeEventListener('touchstart', onPointerDown);
};
}, [open, anchorRef]);
useEffect(() => {
if (!open) {
return;
}
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
onCloseRef.current();
anchorRef?.current?.focus();
}
};
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, [open, anchorRef]);
if (!open) {
return null;
}
return (
<div
ref={panelRef}
role="dialog"
aria-label={label}
className={`absolute z-40 w-64 rounded-[var(--radius-md)] border border-border/60 bg-surface-2 p-2 shadow-[var(--shadow-3)] ${
placement === 'top' ? 'gl-popover bottom-full mb-2' : 'gl-popover-down top-full mt-2'
} ${align === 'right' ? 'right-0' : 'left-0'} ${className}`}
>
{children}
</div>
);
}