F: Improve layout by reworking dropdowns

This commit is contained in:
Ivan 2025-03-07 02:45:37 +03:00
parent ad1d1a47d6
commit fce995f27d
25 changed files with 122 additions and 83 deletions

View File

@ -58,11 +58,10 @@ export function Navigation() {
>
<Logo />
</div>
<div className='flex gap-1 py-[0.3rem]'>
<div className='flex gap-2 items-center'>
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
<UserMenu />
</div>
</div>

View File

@ -6,30 +6,29 @@ import { globalIDs } from '@/utils/constants';
interface NavigationButtonProps extends Styling {
text?: string;
title?: string;
hideTitle?: boolean;
icon: React.ReactNode;
onClick?: (event: React.MouseEvent<Element>) => void;
}
export function NavigationButton({ icon, title, className, style, onClick, text }: NavigationButtonProps) {
export function NavigationButton({ icon, title, hideTitle, className, style, onClick, text }: NavigationButtonProps) {
return (
<button
type='button'
tabIndex={-1}
aria-label={title}
data-tooltip-id={!!title ? globalIDs.tooltip : undefined}
data-tooltip-hidden={hideTitle}
data-tooltip-content={title}
onClick={onClick}
className={clsx(
'mr-1 h-full',
'p-2 h-min',
'flex items-center gap-1',
'cursor-pointer',
'clr-btn-nav cc-animate-color duration-500',
'rounded-xl',
'font-controls whitespace-nowrap',
{
'px-2': text,
'px-4': !text
},
className
)}
style={style}

View File

@ -2,6 +2,7 @@ import { useAuthSuspense } from '@/features/auth';
import { IconLogin, IconUser2 } from '@/components/Icons';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';
import { NavigationButton } from './NavigationButton';
@ -28,8 +29,10 @@ export function UserButton({ onLogin, onClickUser, isOpen }: UserButtonProps) {
<NavigationButton
className='cc-fade-in'
title='Пользователь'
hideTitle={isOpen}
aria-haspopup='true'
aria-expanded={isOpen}
aria-controls={globalIDs.user_dropdown}
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
onClick={onClickUser}
/>

View File

@ -17,6 +17,7 @@ import {
IconUser
} from '@/components/Icons';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';
import { urls } from '../urls';
@ -75,7 +76,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
}
return (
<Dropdown className='mt-[1.5rem] min-w-[18ch] max-w-[12rem]' stretchLeft isOpen={isOpen}>
<Dropdown id={globalIDs.user_dropdown} className='min-w-[18ch] max-w-[12rem]' stretchLeft isOpen={isOpen}>
<DropdownButton
text={user.username}
title='Профиль пользователя'

View File

@ -13,7 +13,7 @@ export function UserMenu() {
const router = useConceptNavigation();
const menu = useDropdown();
return (
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
<div ref={menu.ref} className='flex items-center justify-start relative h-full pr-2'>
<Suspense fallback={<Loader circular scale={1.5} />}>
<UserButton
onLogin={() => router.push({ path: urls.login, force: true })}

View File

@ -5,6 +5,12 @@ import { PARAMETER } from '@/utils/constants';
import { type Styling } from '../props';
interface DropdownProps extends Styling {
/** Unique ID for the dropdown. */
id?: string;
/** Margin for the dropdown. */
margin?: string;
/** Indicates whether the dropdown should stretch to the left. */
stretchLeft?: boolean;
@ -22,18 +28,18 @@ export function Dropdown({
isOpen,
stretchLeft,
stretchTop,
margin,
className,
children,
style,
...restProps
}: React.PropsWithChildren<DropdownProps>) {
return (
<div className='relative'>
<div
tabIndex={-1}
className={clsx(
'z-topmost',
'absolute mt-3',
'absolute',
'flex flex-col',
'border rounded-md shadow-lg',
'text-sm',
@ -41,8 +47,10 @@ export function Dropdown({
{
'right-0': stretchLeft,
'left-0': !stretchLeft,
'bottom-[2rem]': stretchTop
'bottom-0': stretchTop,
'top-full': !stretchTop
},
margin,
className
)}
style={{
@ -59,6 +67,5 @@ export function Dropdown({
>
{children}
</div>
</div>
);
}

View File

@ -44,7 +44,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
}
return (
<div ref={accessMenu.ref}>
<div ref={accessMenu.ref} className='relative'>
<Button
dense
noBorder
@ -56,7 +56,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
icon={<IconRole role={role} size='1.25rem' />}
onClick={accessMenu.toggle}
/>
<Dropdown isOpen={accessMenu.isOpen}>
<Dropdown isOpen={accessMenu.isOpen} margin='mt-3'>
<DropdownButton
text={labelUserRole(UserRole.READER)}
title={describeUserRole(UserRole.READER)}

View File

@ -28,7 +28,7 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
}
return (
<div ref={ossMenu.ref} className={clsx('flex items-center', className)} {...restProps}>
<div ref={ossMenu.ref} className={clsx('relative flex items-center', className)} {...restProps}>
<MiniButton
icon={<IconOSS size='1.25rem' className='icon-primary' />}
title='Операционные схемы'
@ -36,7 +36,7 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
onClick={onToggle}
/>
{items.length > 1 ? (
<Dropdown isOpen={ossMenu.isOpen}>
<Dropdown isOpen={ossMenu.isOpen} margin='mt-1'>
<Label text='Список ОСС' className='border-b px-3 py-1' />
{items.map((reference, index) => (
<DropdownButton

View File

@ -113,14 +113,14 @@ export function PickSchema({
query={filterText}
onChangeQuery={newValue => setFilterText(newValue)}
/>
<div ref={locationMenu.ref}>
<div className='relative' ref={locationMenu.ref}>
<MiniButton
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
title='Фильтр по расположению'
className='mt-1'
onClick={() => locationMenu.toggle()}
/>
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modal-tooltip mt-0'>
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modal-tooltip'>
<SelectLocation
value={filterLocation}
prefix={prefixes.folders_list}

View File

@ -1,5 +1,7 @@
'use client';
import clsx from 'clsx';
import { MiniButton } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { type Styling } from '@/components/props';
@ -18,7 +20,14 @@ interface SelectAccessPolicyProps extends Styling {
stretchLeft?: boolean;
}
export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
export function SelectAccessPolicy({
value,
disabled,
className,
stretchLeft,
onChange,
...restProps
}: SelectAccessPolicyProps) {
const menu = useDropdown();
function handleChange(newValue: AccessPolicy) {
@ -29,7 +38,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
}
return (
<div ref={menu.ref} {...restProps}>
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
<MiniButton
title={`Доступ: ${labelAccessPolicy(value)}`}
hideTitle={menu.isOpen}
@ -38,7 +47,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
onClick={menu.toggle}
disabled={disabled}
/>
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft}>
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft} margin='mt-1'>
{Object.values(AccessPolicy).map((item, index) => (
<DropdownButton
key={`${prefixes.policy_list}${index}`}

View File

@ -1,5 +1,7 @@
'use client';
import clsx from 'clsx';
import { SelectorButton } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { type Styling } from '@/components/props';
@ -17,7 +19,14 @@ interface SelectItemTypeProps extends Styling {
stretchLeft?: boolean;
}
export function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
export function SelectItemType({
value,
disabled,
className,
stretchLeft,
onChange,
...restProps
}: SelectItemTypeProps) {
const menu = useDropdown();
function handleChange(newValue: LibraryItemType) {
@ -28,7 +37,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
}
return (
<div ref={menu.ref} {...restProps}>
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
<SelectorButton
transparent
title={describeLibraryItemType(value)}
@ -39,7 +48,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
onClick={menu.toggle}
disabled={disabled}
/>
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft}>
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft} margin='mt-1'>
{Object.values(LibraryItemType).map((item, index) => (
<DropdownButton
key={`${prefixes.policy_list}${index}`}

View File

@ -14,7 +14,7 @@ interface SelectLocationContextProps extends Styling {
value: string;
onChange: (newValue: string) => void;
title?: string;
stretchTop?: boolean;
dropdownHeight?: string;
}
export function SelectLocationContext({
@ -22,7 +22,8 @@ export function SelectLocationContext({
title = 'Проводник...',
onChange,
className,
style
dropdownHeight,
...restProps
}: SelectLocationContextProps) {
const menu = useDropdown();
@ -34,7 +35,11 @@ export function SelectLocationContext({
}
return (
<div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'>
<div
ref={menu.ref}
className={clsx('relative h-full mt-[-0.25rem] ml-[-1.5rem]', 'text-right self-start', className)}
{...restProps}
>
<MiniButton
title={title}
hideTitle={menu.isOpen}
@ -43,8 +48,8 @@ export function SelectLocationContext({
/>
<Dropdown
isOpen={menu.isOpen}
className={clsx('w-[20rem] h-[12.5rem] z-modal-tooltip mt-[-0.25rem]', className)}
style={style}
className={clsx('w-[20rem] h-[12.5rem] z-modal-tooltip', dropdownHeight)}
margin='mt-[-0.25rem]'
>
<SelectLocation
value={value}

View File

@ -33,7 +33,7 @@ export function SelectLocationHead({
}
return (
<div ref={menu.ref} className={clsx('h-full text-right', className)} {...restProps}>
<div ref={menu.ref} className={clsx('h-full text-right relative', className)} {...restProps}>
<SelectorButton
transparent
tabIndex={-1}
@ -45,7 +45,7 @@ export function SelectLocationHead({
onClick={menu.toggle}
/>
<Dropdown isOpen={menu.isOpen} className='z-modal-tooltip'>
<Dropdown isOpen={menu.isOpen} className='z-modal-tooltip' margin='mt-2'>
{Object.values(LocationHead)
.filter(head => !excluded.includes(head))
.map((head, index) => {

View File

@ -70,9 +70,10 @@ export function ToolbarItemAccess({
onClick={toggleReadOnly}
disabled={role === UserRole.READER || isProcessing}
/>
<div className='pt-[0.125rem]'>
<BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} />
</div>
</div>
</Overlay>
);
}

View File

@ -77,7 +77,7 @@ export function DlgChangeLocation() {
control={control}
name='location'
render={({ field }) => (
<SelectLocationContext className='max-h-[9.2rem]' value={field.value} onChange={field.onChange} />
<SelectLocationContext dropdownHeight='max-h-[9.2rem]' value={field.value} onChange={field.onChange} />
)}
/>
<Controller

View File

@ -103,14 +103,14 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
onClick={toggleVisible}
/>
<div ref={userMenu.ref} className='flex'>
<div ref={userMenu.ref} className='relative flex'>
<MiniButton
title='Поиск пользователя'
hideTitle={userMenu.isOpen}
icon={<IconUserSearch size='1.25rem' className={userActive ? 'icon-green' : 'icon-primary'} />}
onClick={userMenu.toggle}
/>
<Dropdown isOpen={userMenu.isOpen}>
<Dropdown isOpen={userMenu.isOpen} margin='mt-[0.2rem]'>
<DropdownButton
text='Я - Владелец'
icon={<IconOwner size='1.25rem' className={tripleToggleColor(isOwned)} />}
@ -149,7 +149,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
onChangeQuery={setQuery}
/>
{!folderMode ? (
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
<div ref={headMenu.ref} className='relative flex items-center h-full py-1 select-none'>
<SelectorButton
transparent
className='h-full rounded-lg'

View File

@ -172,6 +172,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
isOpen={isOpen}
stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH}
stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT}
margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'}
>
<DropdownButton
text='Редактировать'

View File

@ -31,7 +31,7 @@ export function MenuEditOss() {
}
return (
<div ref={editMenu.ref}>
<div ref={editMenu.ref} className='relative'>
<Button
dense
noBorder
@ -43,7 +43,7 @@ export function MenuEditOss() {
icon={<IconEdit2 size='1.25rem' className={isMutable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle}
/>
<Dropdown isOpen={editMenu.isOpen}>
<Dropdown isOpen={editMenu.isOpen} margin='mt-3'>
<DropdownButton
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'

View File

@ -46,7 +46,7 @@ export function MenuMain() {
}
return (
<div ref={schemaMenu.ref}>
<div ref={schemaMenu.ref} className='relative'>
<Button
dense
noBorder
@ -58,7 +58,7 @@ export function MenuMain() {
className='h-full pl-2'
onClick={schemaMenu.toggle}
/>
<Dropdown isOpen={schemaMenu.isOpen}>
<Dropdown isOpen={schemaMenu.isOpen} margin='mt-3'>
<DropdownButton
text='Поделиться'
icon={<IconShare size='1rem' className='icon-primary' />}

View File

@ -69,7 +69,7 @@ export function ToolbarRSList() {
disabled={isProcessing || selected.length === 0 || selected.length === schema.items.length}
onClick={moveDown}
/>
<div ref={insertMenu.ref}>
<div ref={insertMenu.ref} className='relative'>
<MiniButton
title='Добавить пустую конституенту'
hideTitle={insertMenu.isOpen}
@ -77,7 +77,7 @@ export function ToolbarRSList() {
disabled={isProcessing}
onClick={insertMenu.toggle}
/>
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2 md:translate-x-0'>
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2'>
{Object.values(CstType).map(typeStr => (
<DropdownButton
key={`${prefixes.csttype_list}${typeStr}`}

View File

@ -118,7 +118,7 @@ export function MenuEditSchema() {
}
return (
<div ref={editMenu.ref}>
<div ref={editMenu.ref} className='relative'>
<Button
dense
noBorder
@ -130,7 +130,7 @@ export function MenuEditSchema() {
icon={<IconEdit2 size='1.25rem' className={isContentEditable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle}
/>
<Dropdown isOpen={editMenu.isOpen}>
<Dropdown isOpen={editMenu.isOpen} margin='mt-3'>
<DropdownButton
text='Шаблоны'
title='Создать конституенту из шаблона'

View File

@ -113,7 +113,7 @@ export function MenuMain() {
}
return (
<div ref={schemaMenu.ref}>
<div ref={schemaMenu.ref} className='relative'>
<Button
dense
noBorder
@ -125,7 +125,7 @@ export function MenuMain() {
className='h-full pl-2'
onClick={schemaMenu.toggle}
/>
<Dropdown isOpen={schemaMenu.isOpen}>
<Dropdown isOpen={schemaMenu.isOpen} margin='mt-3'>
<DropdownButton
text='Поделиться'
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}

View File

@ -1,5 +1,7 @@
'use client';
import clsx from 'clsx';
import { SelectorButton } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { type Styling } from '@/components/props';
@ -16,7 +18,7 @@ interface SelectGraphFilterProps extends Styling {
dense?: boolean;
}
export function SelectGraphFilter({ value, dense, onChange, ...restProps }: SelectGraphFilterProps) {
export function SelectGraphFilter({ value, dense, className, onChange, ...restProps }: SelectGraphFilterProps) {
const menu = useDropdown();
const size = useWindowSize();
@ -26,7 +28,7 @@ export function SelectGraphFilter({ value, dense, onChange, ...restProps }: Sele
}
return (
<div ref={menu.ref} {...restProps}>
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
<SelectorButton
transparent
tabIndex={-1}
@ -37,7 +39,7 @@ export function SelectGraphFilter({ value, dense, onChange, ...restProps }: Sele
text={!dense && !size.isSmall ? labelCstSource(value) : undefined}
onClick={menu.toggle}
/>
<Dropdown stretchLeft isOpen={menu.isOpen}>
<Dropdown stretchLeft isOpen={menu.isOpen} margin='mt-3'>
{Object.values(DependencyMode)
.filter(value => !isNaN(Number(value)))
.map((value, index) => {

View File

@ -1,5 +1,7 @@
'use client';
import clsx from 'clsx';
import { SelectorButton } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { type Styling } from '@/components/props';
@ -16,7 +18,7 @@ interface SelectMatchModeProps extends Styling {
dense?: boolean;
}
export function SelectMatchMode({ value, dense, onChange, ...restProps }: SelectMatchModeProps) {
export function SelectMatchMode({ value, dense, className, onChange, ...restProps }: SelectMatchModeProps) {
const menu = useDropdown();
const size = useWindowSize();
@ -26,7 +28,7 @@ export function SelectMatchMode({ value, dense, onChange, ...restProps }: Select
}
return (
<div ref={menu.ref} {...restProps}>
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
<SelectorButton
transparent
titleHtml='Настройка фильтрации <br/>по проверяемым атрибутам'
@ -36,7 +38,7 @@ export function SelectMatchMode({ value, dense, onChange, ...restProps }: Select
text={dense || size.isSmall ? undefined : labelCstMatchMode(value)}
onClick={menu.toggle}
/>
<Dropdown stretchLeft isOpen={menu.isOpen}>
<Dropdown stretchLeft isOpen={menu.isOpen} margin='mt-3'>
{Object.values(CstMatchMode)
.filter(value => !isNaN(Number(value)))
.map((value, index) => {

View File

@ -102,7 +102,8 @@ export const globalIDs = {
email_tooltip: 'email_tooltip',
library_item_editor: 'library_item_editor',
constituenta_editor: 'constituenta_editor',
graph_schemas: 'graph_schemas_tooltip'
graph_schemas: 'graph_schemas_tooltip',
user_dropdown: 'user_dropdown'
};
/**