fix(web): keep menu labels on one line and fold the admin section by default
ci / ts / ts (packages + web) (push) Successful in 1m27s
ci / ts / ts (packages + web) (push) Successful in 1m27s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
dfc72b1d69
commit
2b76060ea3
4 files changed
+34
-9
No files matched your search
@@ -117,7 +117,8 @@ describe('navigation by role', () => {
|
||||
|
||||
it('shows users but not the audit log to moderators', async () => {
|
||||
const mod = makeUser('mod', 'moderator');
|
||||
await renderApp([mod], '/', { signedInAs: mod });
|
||||
const { user } = await renderApp([mod], '/', { signedInAs: mod });
|
||||
await user.click(await screen.findByRole('button', { name: 'Админка' }));
|
||||
expect(await screen.findByRole('link', { name: 'Пользователи' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: 'Журнал' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -268,7 +268,9 @@
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
z-index: 40;
|
||||
width: max-content;
|
||||
min-width: 220px;
|
||||
max-width: min(360px, calc(100vw - 24px));
|
||||
padding: 6px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--elev);
|
||||
@@ -292,6 +294,7 @@
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
.profileHead {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-columns: auto max-content;
|
||||
gap: 4px 12px;
|
||||
align-items: center;
|
||||
padding: 10px 10px 12px;
|
||||
@@ -100,11 +100,13 @@
|
||||
|
||||
.profileName {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profileMeta {
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.side {
|
||||
|
||||
@@ -79,25 +79,44 @@ function NavLink({
|
||||
}
|
||||
|
||||
const FOLDED_KEY = 'pipu.nav.folded';
|
||||
const TOUCHED_KEY = 'pipu.nav.touched';
|
||||
|
||||
function readFolded(): string[] {
|
||||
function readIds(key: string): string[] {
|
||||
try {
|
||||
const v: unknown = JSON.parse(localStorage.getItem(FOLDED_KEY) ?? '[]');
|
||||
const v: unknown = JSON.parse(localStorage.getItem(key) ?? '[]');
|
||||
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Раздел меню: заголовок сворачивает пункты; состояние помнит браузер. Свёрнутый раздел показывает сумму бейджей. */
|
||||
function NavSection({ id, title, badge, children }: { id: string; title: string; badge?: number; children: ReactNode }) {
|
||||
const [folded, setFolded] = useState(() => readFolded().includes(id));
|
||||
/** Раздел меню: заголовок сворачивает пункты; явный выбор помнит браузер. Свёрнутый раздел показывает сумму бейджей. */
|
||||
function NavSection({
|
||||
id,
|
||||
title,
|
||||
badge,
|
||||
children,
|
||||
defaultFolded = false,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
badge?: number;
|
||||
children: ReactNode;
|
||||
defaultFolded?: boolean;
|
||||
}) {
|
||||
const [folded, setFolded] = useState(() => {
|
||||
if (readIds(FOLDED_KEY).includes(id)) return true;
|
||||
if (readIds(TOUCHED_KEY).includes(id)) return false;
|
||||
return defaultFolded;
|
||||
});
|
||||
const toggle = () => {
|
||||
const next = !folded;
|
||||
setFolded(next);
|
||||
try {
|
||||
const rest = readFolded().filter((x) => x !== id);
|
||||
const rest = readIds(FOLDED_KEY).filter((x) => x !== id);
|
||||
localStorage.setItem(FOLDED_KEY, JSON.stringify(next ? [...rest, id] : rest));
|
||||
const touched = readIds(TOUCHED_KEY);
|
||||
if (!touched.includes(id)) localStorage.setItem(TOUCHED_KEY, JSON.stringify([...touched, id]));
|
||||
} catch {
|
||||
// Без хранилища раздел просто не запомнит состояние.
|
||||
}
|
||||
@@ -188,7 +207,7 @@ function Shell() {
|
||||
/>
|
||||
</NavSection>
|
||||
{staff && (
|
||||
<NavSection id="admin" title={t('nav.admin')} badge={pending}>
|
||||
<NavSection id="admin" title={t('nav.admin')} badge={pending} defaultFolded>
|
||||
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
||||
{user.role === 'admin' && (
|
||||
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
||||
|
||||
Reference in new issue
Block a user