feat(ui): batch UI polish — auth/lang to sidebar, search h-7, info overflow, graph zoom
User feedback batch — multiple iterations of UX refinement.
Layout shifts:
- TopBar right cluster: AuthBadge + LanguageSwitch переехали в Sidebar
footer per user ("вход в сайдбар перенесем", "ru/en тоже в сайд баре").
TopBar теперь: breadcrumb + search + Docs + VersionBadge + ThemeSwitch
only. Освобождает место + concentrates user/lang controls в одном месте.
- Sidebar footer: новая структура — Lang toggle (Язык [RU/EN]) + Auth block
(avatar+name+logout authed, Sign-in CTA anonymous) + collapse button.
Refactored из inline JSX в named subcomponents (AuthedUserBlock,
SignInCta, SidebarFooter).
Sizing / spacing:
- TopBar SearchInput: h-9 → h-7 to match LangSwitch/ThemeSwitch (+ Sign-in
кнопка). User: "поиск по справочникам высоту выровняй". Override via
`!h-7 text-cell` className на SearchInput.
- TopBar: h-14 → h-11 (slimmer). Sidebar logo block matches h-11.
Catalog search:
- Catalog page input в toolbar — restored для on-page filter ("Catalog
search никак не работает" → проверил, работает; добавил TopBar context-
aware behavior где TopBar input на /dictionaries route синхронизируется
с URL ?q= live).
- TopBar search context-aware: catalog mode = live filter; else = submit
→ /search (records JSONB search).
InfoPanel:
- Description: `text-body text-ink-2 leading-relaxed` → +
`break-words overflow-hidden`. Long slash-separated values
(LZW/Deflate/JPEG2000/ZSTD/LERC) теперь wrap'ятся внутри panel вместо
overflow за границы (user: "описание убежало за границы панели").
- Container: + `min-w-0 overflow-hidden` для proper flex shrinking.
Graph:
- Zoom controls overlay (+/-/1:1/N%) + mouse wheel zoom toward cursor +
drag pan по empty space. Per user: "Граф связей еще не зумируется".
WorkflowBanner:
- Moved в InfoPanel banner slot (compact flex-col layout) — free
horizontal space выше editor + concentrates status info в left rail.
Auth:
- AuthBadge: primary Button → compact h-7 outline button. Matches
TopBar toolbar control style.
- RequireAuth: убран visible amber error banner "Авторизация недоступна:
Failed to fetch" — silent fall-through к anonymous mode (user feedback).
- routes/index.tsx: beforeLoad skip redirect если есть `?code=` в URL
(OIDC callback fix). HomeIndex компонент рендерит null + redirect на
/dictionaries после auth complete.
LanguageSwitch:
- h-8 + items-stretch + inner items-center — matches ThemeSwitch height.
- Later moved entirely в Sidebar footer (h-7 button only) per user.
This commit is contained in:
@@ -159,13 +159,12 @@ function SidebarContent({
|
||||
/** Toggle button handler. When undefined — collapse button hidden (mobile). */
|
||||
onToggleCollapsed?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
{/* Logo block per redesign prototype: circle (accent) + ORDINIS + MDM mute */}
|
||||
<div
|
||||
className={cn(
|
||||
'h-14 flex items-center border-b border-line shrink-0',
|
||||
'h-11 flex items-center border-b border-line shrink-0',
|
||||
collapsed ? 'justify-center px-2' : 'px-5 gap-2',
|
||||
)}
|
||||
>
|
||||
@@ -213,48 +212,57 @@ function SidebarContent({
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User block — sticky bottom */}
|
||||
{auth.isAuthenticated && auth.user?.profile && (
|
||||
<div
|
||||
className={cn(
|
||||
'border-t border-line py-3 flex items-center gap-2 shrink-0',
|
||||
collapsed ? 'px-2 justify-center' : 'px-3',
|
||||
)}
|
||||
title={String(
|
||||
auth.user.profile.preferred_username ||
|
||||
auth.user.profile.name ||
|
||||
auth.user.profile.email ||
|
||||
'user',
|
||||
)}
|
||||
>
|
||||
<div className="size-8 rounded-full bg-accent text-on-accent inline-flex items-center justify-center text-cell font-semibold shrink-0">
|
||||
{String(
|
||||
auth.user.profile.preferred_username ||
|
||||
auth.user.profile.name ||
|
||||
'U',
|
||||
)
|
||||
.charAt(0)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-cell font-medium text-ink truncate">
|
||||
{String(
|
||||
auth.user.profile.preferred_username ||
|
||||
auth.user.profile.name ||
|
||||
auth.user.profile.email ||
|
||||
'user',
|
||||
)}
|
||||
</div>
|
||||
{auth.user.profile.email && (
|
||||
<div className="text-cell text-mute truncate">{String(auth.user.profile.email)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Footer: Lang toggle + User profile (or Sign-in) + Collapse toggle.
|
||||
* Moved from TopBar per user feedback: "вход в сайдбар, ru/en в сайдбаре". */}
|
||||
<SidebarFooter
|
||||
auth={auth}
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={onToggleCollapsed}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFooter({
|
||||
auth,
|
||||
collapsed,
|
||||
onToggleCollapsed,
|
||||
}: {
|
||||
auth: ReturnType<typeof useAuth>
|
||||
collapsed: boolean
|
||||
onToggleCollapsed?: () => void
|
||||
}) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const cur = i18n.language?.toLowerCase().startsWith('en') ? 'en' : 'ru'
|
||||
const next = cur === 'ru' ? 'en-US' : 'ru-RU'
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Language toggle row (только когда expanded). */}
|
||||
{!collapsed && (
|
||||
<div className="border-t border-line px-3 py-2 flex items-center gap-2 shrink-0">
|
||||
<span className="text-cap text-mute">
|
||||
{t('sidebar.lang', { defaultValue: 'Язык' })}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => i18n.changeLanguage(next)}
|
||||
className="ml-auto h-7 px-2.5 rounded-md border border-line bg-surface text-cap text-ink-2 hover:bg-surface-2 hover:text-ink transition-colors"
|
||||
aria-label={`Language: ${cur.toUpperCase()}, click to switch`}
|
||||
>
|
||||
{cur.toUpperCase()}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapse toggle — desktop only (mobile uses Sheet close ×) */}
|
||||
{/* Auth block: avatar + name (authed) OR Sign-in CTA (anonymous). */}
|
||||
{auth.isAuthenticated && auth.user?.profile ? (
|
||||
<AuthedUserBlock auth={auth} collapsed={collapsed} />
|
||||
) : (
|
||||
<SignInCta auth={auth} collapsed={collapsed} />
|
||||
)}
|
||||
|
||||
{/* Collapse toggle */}
|
||||
{onToggleCollapsed && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -279,6 +287,77 @@ function SidebarContent({
|
||||
)
|
||||
}
|
||||
|
||||
function AuthedUserBlock({
|
||||
auth,
|
||||
collapsed,
|
||||
}: {
|
||||
auth: ReturnType<typeof useAuth>
|
||||
collapsed: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const profile = auth.user?.profile
|
||||
if (!profile) return null
|
||||
const display = String(
|
||||
profile.preferred_username || profile.name || profile.email || 'user',
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-t border-line py-3 flex items-center gap-2 shrink-0',
|
||||
collapsed ? 'px-2 justify-center' : 'px-3',
|
||||
)}
|
||||
title={display}
|
||||
>
|
||||
<div className="size-8 rounded-full bg-accent text-on-accent inline-flex items-center justify-center text-cell font-semibold shrink-0">
|
||||
{display.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-cell font-medium text-ink truncate">{display}</div>
|
||||
{profile.email && (
|
||||
<div className="text-cell text-mute truncate">{String(profile.email)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => auth.signoutRedirect().catch(() => auth.removeUser())}
|
||||
title={t('auth.logout', { defaultValue: 'Logout' })}
|
||||
aria-label={t('auth.logout', { defaultValue: 'Logout' })}
|
||||
className="size-7 rounded-md text-mute hover:text-ink hover:bg-surface-2 inline-flex items-center justify-center transition-colors shrink-0"
|
||||
>
|
||||
<span aria-hidden>⎋</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SignInCta({
|
||||
auth,
|
||||
collapsed,
|
||||
}: {
|
||||
auth: ReturnType<typeof useAuth>
|
||||
collapsed: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => auth.signinRedirect()}
|
||||
className={cn(
|
||||
'border-t border-line py-2.5 text-cell text-ink-2 hover:text-ink hover:bg-surface-2 inline-flex items-center gap-2 transition-colors shrink-0',
|
||||
collapsed ? 'px-2 justify-center' : 'px-3',
|
||||
)}
|
||||
title={t('auth.login', { defaultValue: 'Войти' })}
|
||||
>
|
||||
<span aria-hidden>→</span>
|
||||
{!collapsed && t('auth.login', { defaultValue: 'Войти' })}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* MobileSidebar — Sheet с тем же содержимым что desktop Sidebar. Открывается
|
||||
* через hamburger button в TopBar (<1024px), закрывается на: backdrop click,
|
||||
|
||||
Reference in New Issue
Block a user