feat(admin): Control Center redesign, Explore dual-view и полный RU/EN i18n
CI / test (push) Failing after 3m9s
CI / push-image (push) Has been skipped
CI / ci-done (push) Failing after 0s

Refs EventHub/EventHubFrontAdmin#32
This commit is contained in:
2026-07-14 17:58:33 +03:00
parent de379e28bf
commit fd2c1f10ad
95 changed files with 12699 additions and 4410 deletions
+72
View File
@@ -0,0 +1,72 @@
import { useEffect } from 'react';
/**
* Горячие клавиши inbox (когда фокус не в input/textarea/select/contenteditable).
* j/k — соседний элемент, 1/2 — действия, Esc — снять фокус списка (опционально).
*/
export function useInboxHotkeys(options: {
enabled?: boolean;
onNext: () => void;
onPrev: () => void;
onPrimary?: () => void;
onSecondary?: () => void;
onOpen?: () => void;
}) {
const { enabled = true, onNext, onPrev, onPrimary, onSecondary, onOpen } = options;
useEffect(() => {
if (!enabled) return;
const isTypingTarget = (el: EventTarget | null) => {
if (!(el instanceof HTMLElement)) return false;
const tag = el.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
};
const handler = (e: KeyboardEvent) => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
if (isTypingTarget(e.target)) return;
const key = e.key.toLowerCase();
if (key === 'j' || key === 'arrowdown') {
e.preventDefault();
onNext();
} else if (key === 'k' || key === 'arrowup') {
e.preventDefault();
onPrev();
} else if (key === 'enter' && onOpen) {
e.preventDefault();
onOpen();
} else if (key === '1' && onPrimary) {
e.preventDefault();
onPrimary();
} else if (key === '2' && onSecondary) {
e.preventDefault();
onSecondary();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [enabled, onNext, onPrev, onPrimary, onSecondary, onOpen]);
}
export function selectNeighborId(
ids: string[],
currentId: string,
direction: 1 | -1
): string | null {
if (ids.length === 0) return null;
const idx = ids.indexOf(currentId);
if (idx < 0) return ids[0] ?? null;
const next = idx + direction;
if (next < 0 || next >= ids.length) return currentId;
return ids[next];
}
/** Следующий после текущего; если конец — предыдущий. */
export function selectAfterRemove(ids: string[], currentId: string): string | null {
const idx = ids.indexOf(currentId);
if (idx < 0) return ids[0] ?? null;
return ids[idx + 1] ?? ids[idx - 1] ?? null;
}