feat: port stroy-bot-main as the hub page

Project picker, section cards and the period Excel export now live on the
app's home page. The selected project is shared app state, so the sidebar
hides sections with the same rules main used to disable its cards
(archived, finished, no edit0 right).

Queries are 1:1 with stroy-bot-main and covered by tests against a fake
HTTP client. Behaviour differences are listed in the migration checklist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-15 16:49:32 +05:00
parent b868e8012d
commit bed230f045
32 changed files with 1626 additions and 6 deletions
@@ -0,0 +1,36 @@
import * as React from 'react';
/**
* Закрывает всплывающий элемент по клику вне его и по Esc.
* Слушатели вешаются только пока элемент открыт и снимаются при закрытии/размонтировании.
*/
export function useOutsideClose(
ref: React.RefObject<HTMLElement>,
isOpen: boolean,
onClose: (reason: 'outside' | 'escape') => void
): void {
const onCloseRef = React.useRef(onClose);
onCloseRef.current = onClose;
React.useEffect(() => {
if (!isOpen) {
return undefined;
}
const onClick = (event: MouseEvent): void => {
if (ref.current && !ref.current.contains(event.target as Node)) {
onCloseRef.current('outside');
}
};
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
onCloseRef.current('escape');
}
};
document.addEventListener('click', onClick);
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('click', onClick);
document.removeEventListener('keydown', onKeyDown);
};
}, [isOpen, ref]);
}