d9df2fc359
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.
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { cn } from '@/lib/utils'
|
|
|
|
/**
|
|
* LanguageSwitch — segmented control для смены i18next language.
|
|
* Замена @nstart/ui LanguageSwitch (тот же value/options/onChange API).
|
|
*
|
|
* <p>Compact: 3-letter codes (RU/EN/ZH...) в Tektur uppercase. Tab-like
|
|
* underline indicator под active. На narrow screens сворачивается в icon
|
|
* только (TODO mobile responsive).
|
|
*/
|
|
|
|
/** nstart-compat: {id, label}. `value` accepted as alias. */
|
|
export type LanguageOption = {
|
|
id: string
|
|
label: string
|
|
/** Alias of `id`. */
|
|
value?: string
|
|
/** Short 2-3 char code (RU/EN/ZH). Default — id.toUpperCase(). */
|
|
short?: string
|
|
}
|
|
|
|
export type LanguageSwitchProps = {
|
|
value: string
|
|
options: LanguageOption[]
|
|
onChange: (id: string) => void
|
|
className?: string
|
|
'aria-label'?: string
|
|
}
|
|
|
|
export function LanguageSwitch({
|
|
value,
|
|
options,
|
|
onChange,
|
|
className,
|
|
'aria-label': ariaLabel,
|
|
}: LanguageSwitchProps) {
|
|
return (
|
|
<div
|
|
role="radiogroup"
|
|
aria-label={ariaLabel ?? 'Language'}
|
|
className={cn(
|
|
// h-8 box to match TopBar toolbar height. Buttons fill 100% height
|
|
// через inline-flex + items-center — никакого vertical jitter
|
|
// от text baseline vs icon baseline.
|
|
'inline-flex items-stretch rounded-md border border-line bg-surface h-7 overflow-hidden',
|
|
className,
|
|
)}
|
|
>
|
|
{options.map((opt) => {
|
|
const v = opt.id ?? opt.value!
|
|
const active = v === value
|
|
return (
|
|
<button
|
|
key={v}
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={active}
|
|
onClick={() => onChange(v)}
|
|
title={opt.label}
|
|
className={cn(
|
|
'inline-flex items-center justify-center px-2.5 text-cap leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
active
|
|
? 'bg-accent-bg text-accent'
|
|
: 'text-mute hover:text-ink hover:bg-surface-2',
|
|
)}
|
|
>
|
|
{/* Display priority: short → label (if compact like "RU"/"EN") →
|
|
* uppercase id. Label predominates когда caller передал human
|
|
* label типа "RU"/"EN" в options. */}
|
|
{opt.short ?? opt.label ?? v.toUpperCase()}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|